diff options
Diffstat (limited to 'widget/windows/TSFTextStore.cpp')
-rw-r--r-- | widget/windows/TSFTextStore.cpp | 7513 |
1 files changed, 7513 insertions, 0 deletions
diff --git a/widget/windows/TSFTextStore.cpp b/widget/windows/TSFTextStore.cpp new file mode 100644 index 0000000000..b3e3a164ae --- /dev/null +++ b/widget/windows/TSFTextStore.cpp @@ -0,0 +1,7513 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#define INPUTSCOPE_INIT_GUID +#define TEXTATTRS_INIT_GUID +#include "TSFTextStore.h" + +#include <algorithm> +#include <comutil.h> // for _bstr_t +#include <oleauto.h> // for SysAllocString +#include <olectl.h> +#include "nscore.h" + +#include "IMMHandler.h" +#include "KeyboardLayout.h" +#include "WinIMEHandler.h" +#include "WinUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_intl.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/widget/WinRegistry.h" +#include "nsWindow.h" +#include "nsPrintfCString.h" + +// 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. +mozilla::LazyLogModule gIMELog("IMEHandler"); + +// TODO: GUID_PROP_URL has not been declared in the SDK yet. We should drop the +// `s` prefix after it's released by a new SDK and define it with #if. +static const GUID sGUID_PROP_URL = { + 0xd5138268, + 0xa1bf, + 0x4308, + {0xbc, 0xbf, 0x2e, 0x73, 0x93, 0x98, 0xe2, 0x34}}; + +namespace mozilla { +namespace widget { + +/** + * TSF related code should log its behavior even on release build especially + * in the interface methods. + * + * In interface methods, use LogLevel::Info. + * In internal methods, use LogLevel::Debug for logging normal behavior. + * For logging error, use LogLevel::Error. + * + * When an instance method is called, start with following text: + * "0x%p TSFFoo::Bar(", the 0x%p should be the "this" of the nsFoo. + * after that, start with: + * "0x%p TSFFoo::Bar(" + * In an internal method, start with following text: + * "0x%p TSFFoo::Bar(" + * When a static method is called, start with following text: + * "TSFFoo::Bar(" + */ + +enum class TextInputProcessorID { + // Internal use only. This won't be returned by TSFStaticSink::ActiveTIP(). + eNotComputed, + + // Not a TIP. E.g., simple keyboard layout or IMM-IME. + eNone, + + // Used for other TIPs, i.e., any TIPs which we don't support specifically. + eUnknown, + + // TIP for Japanese. + eMicrosoftIMEForJapanese, + eMicrosoftOfficeIME2010ForJapanese, + eGoogleJapaneseInput, + eATOK2011, + eATOK2012, + eATOK2013, + eATOK2014, + eATOK2015, + eATOK2016, + eATOKUnknown, + eJapanist10, + + // TIP for Traditional Chinese. + eMicrosoftBopomofo, + eMicrosoftChangJie, + eMicrosoftPhonetic, + eMicrosoftQuick, + eMicrosoftNewChangJie, + eMicrosoftNewPhonetic, + eMicrosoftNewQuick, + eFreeChangJie, + + // TIP for Simplified Chinese. + eMicrosoftPinyin, + eMicrosoftPinyinNewExperienceInputStyle, + eMicrosoftWubi, + + // TIP for Korean. + eMicrosoftIMEForKorean, + eMicrosoftOldHangul, + + // Keyman Desktop, which can install various language keyboards. + eKeymanDesktop, +}; + +static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; } + +static void HandleSeparator(nsCString& aDesc) { + if (!aDesc.IsEmpty()) { + aDesc.AppendLiteral(" | "); + } +} + +static const nsCString GetFindFlagName(DWORD aFindFlag) { + nsCString description; + if (!aFindFlag) { + description.AppendLiteral("no flags (0)"); + return description; + } + if (aFindFlag & TS_ATTR_FIND_BACKWARDS) { + description.AppendLiteral("TS_ATTR_FIND_BACKWARDS"); + } + if (aFindFlag & TS_ATTR_FIND_WANT_OFFSET) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_WANT_OFFSET"); + } + if (aFindFlag & TS_ATTR_FIND_UPDATESTART) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_UPDATESTART"); + } + if (aFindFlag & TS_ATTR_FIND_WANT_VALUE) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_WANT_VALUE"); + } + if (aFindFlag & TS_ATTR_FIND_WANT_END) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_WANT_END"); + } + if (aFindFlag & TS_ATTR_FIND_HIDDEN) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_HIDDEN"); + } + if (description.IsEmpty()) { + description.AppendLiteral("Unknown ("); + description.AppendInt(static_cast<uint32_t>(aFindFlag)); + description.Append(')'); + } + return description; +} + +class GetACPFromPointFlagName : public nsAutoCString { + public: + explicit GetACPFromPointFlagName(DWORD aFlags) { + if (!aFlags) { + AppendLiteral("no flags (0)"); + return; + } + if (aFlags & GXFPF_ROUND_NEAREST) { + AppendLiteral("GXFPF_ROUND_NEAREST"); + aFlags &= ~GXFPF_ROUND_NEAREST; + } + if (aFlags & GXFPF_NEAREST) { + HandleSeparator(*this); + AppendLiteral("GXFPF_NEAREST"); + aFlags &= ~GXFPF_NEAREST; + } + if (aFlags) { + HandleSeparator(*this); + AppendLiteral("Unknown("); + AppendInt(static_cast<uint32_t>(aFlags)); + Append(')'); + } + } + virtual ~GetACPFromPointFlagName() {} +}; + +static const char* GetFocusChangeName( + InputContextAction::FocusChange aFocusChange) { + switch (aFocusChange) { + case InputContextAction::FOCUS_NOT_CHANGED: + return "FOCUS_NOT_CHANGED"; + case InputContextAction::GOT_FOCUS: + return "GOT_FOCUS"; + case InputContextAction::LOST_FOCUS: + return "LOST_FOCUS"; + case InputContextAction::MENU_GOT_PSEUDO_FOCUS: + return "MENU_GOT_PSEUDO_FOCUS"; + case InputContextAction::MENU_LOST_PSEUDO_FOCUS: + return "MENU_LOST_PSEUDO_FOCUS"; + case InputContextAction::WIDGET_CREATED: + return "WIDGET_CREATED"; + default: + return "Unknown"; + } +} + +static nsCString GetCLSIDNameStr(REFCLSID aCLSID) { + LPOLESTR str = nullptr; + HRESULT hr = ::StringFromCLSID(aCLSID, &str); + if (FAILED(hr) || !str || !str[0]) { + return ""_ns; + } + + nsCString result; + result = NS_ConvertUTF16toUTF8(str); + ::CoTaskMemFree(str); + return result; +} + +static nsCString GetGUIDNameStr(REFGUID aGUID) { + OLECHAR str[40]; + int len = ::StringFromGUID2(aGUID, str, ArrayLength(str)); + if (!len || !str[0]) { + return ""_ns; + } + + return NS_ConvertUTF16toUTF8(str); +} + +static nsCString GetGUIDNameStrWithTable(REFGUID aGUID) { +#define RETURN_GUID_NAME(aNamedGUID) \ + if (IsEqualGUID(aGUID, aNamedGUID)) { \ + return nsLiteralCString(#aNamedGUID); \ + } + + RETURN_GUID_NAME(GUID_PROP_INPUTSCOPE) + RETURN_GUID_NAME(sGUID_PROP_URL) + RETURN_GUID_NAME(TSATTRID_OTHERS) + RETURN_GUID_NAME(TSATTRID_Font) + RETURN_GUID_NAME(TSATTRID_Font_FaceName) + RETURN_GUID_NAME(TSATTRID_Font_SizePts) + RETURN_GUID_NAME(TSATTRID_Font_Style) + RETURN_GUID_NAME(TSATTRID_Font_Style_Bold) + RETURN_GUID_NAME(TSATTRID_Font_Style_Italic) + RETURN_GUID_NAME(TSATTRID_Font_Style_SmallCaps) + RETURN_GUID_NAME(TSATTRID_Font_Style_Capitalize) + RETURN_GUID_NAME(TSATTRID_Font_Style_Uppercase) + RETURN_GUID_NAME(TSATTRID_Font_Style_Lowercase) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_LasVegasLights) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_BlinkingBackground) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_SparkleText) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_MarchingBlackAnts) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_MarchingRedAnts) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_Shimmer) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_WipeDown) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_WipeRight) + RETURN_GUID_NAME(TSATTRID_Font_Style_Emboss) + RETURN_GUID_NAME(TSATTRID_Font_Style_Engrave) + RETURN_GUID_NAME(TSATTRID_Font_Style_Hidden) + RETURN_GUID_NAME(TSATTRID_Font_Style_Kerning) + RETURN_GUID_NAME(TSATTRID_Font_Style_Outlined) + RETURN_GUID_NAME(TSATTRID_Font_Style_Position) + RETURN_GUID_NAME(TSATTRID_Font_Style_Protected) + RETURN_GUID_NAME(TSATTRID_Font_Style_Shadow) + RETURN_GUID_NAME(TSATTRID_Font_Style_Spacing) + RETURN_GUID_NAME(TSATTRID_Font_Style_Weight) + RETURN_GUID_NAME(TSATTRID_Font_Style_Height) + RETURN_GUID_NAME(TSATTRID_Font_Style_Underline) + RETURN_GUID_NAME(TSATTRID_Font_Style_Underline_Single) + RETURN_GUID_NAME(TSATTRID_Font_Style_Underline_Double) + RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough) + RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough_Single) + RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough_Double) + RETURN_GUID_NAME(TSATTRID_Font_Style_Overline) + RETURN_GUID_NAME(TSATTRID_Font_Style_Overline_Single) + RETURN_GUID_NAME(TSATTRID_Font_Style_Overline_Double) + RETURN_GUID_NAME(TSATTRID_Font_Style_Blink) + RETURN_GUID_NAME(TSATTRID_Font_Style_Subscript) + RETURN_GUID_NAME(TSATTRID_Font_Style_Superscript) + RETURN_GUID_NAME(TSATTRID_Font_Style_Color) + RETURN_GUID_NAME(TSATTRID_Font_Style_BackgroundColor) + RETURN_GUID_NAME(TSATTRID_Text) + RETURN_GUID_NAME(TSATTRID_Text_VerticalWriting) + RETURN_GUID_NAME(TSATTRID_Text_RightToLeft) + RETURN_GUID_NAME(TSATTRID_Text_Orientation) + RETURN_GUID_NAME(TSATTRID_Text_Language) + RETURN_GUID_NAME(TSATTRID_Text_ReadOnly) + RETURN_GUID_NAME(TSATTRID_Text_EmbeddedObject) + RETURN_GUID_NAME(TSATTRID_Text_Alignment) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Left) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Right) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Center) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Justify) + RETURN_GUID_NAME(TSATTRID_Text_Link) + RETURN_GUID_NAME(TSATTRID_Text_Hyphenation) + RETURN_GUID_NAME(TSATTRID_Text_Para) + RETURN_GUID_NAME(TSATTRID_Text_Para_FirstLineIndent) + RETURN_GUID_NAME(TSATTRID_Text_Para_LeftIndent) + RETURN_GUID_NAME(TSATTRID_Text_Para_RightIndent) + RETURN_GUID_NAME(TSATTRID_Text_Para_SpaceAfter) + RETURN_GUID_NAME(TSATTRID_Text_Para_SpaceBefore) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Single) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_OnePtFive) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Double) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_AtLeast) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Exactly) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Multiple) + RETURN_GUID_NAME(TSATTRID_List) + RETURN_GUID_NAME(TSATTRID_List_LevelIndel) + RETURN_GUID_NAME(TSATTRID_List_Type) + RETURN_GUID_NAME(TSATTRID_List_Type_Bullet) + RETURN_GUID_NAME(TSATTRID_List_Type_Arabic) + RETURN_GUID_NAME(TSATTRID_List_Type_LowerLetter) + RETURN_GUID_NAME(TSATTRID_List_Type_UpperLetter) + RETURN_GUID_NAME(TSATTRID_List_Type_LowerRoman) + RETURN_GUID_NAME(TSATTRID_List_Type_UpperRoman) + RETURN_GUID_NAME(TSATTRID_App) + RETURN_GUID_NAME(TSATTRID_App_IncorrectSpelling) + RETURN_GUID_NAME(TSATTRID_App_IncorrectGrammar) + +#undef RETURN_GUID_NAME + + return GetGUIDNameStr(aGUID); +} + +static nsCString GetRIIDNameStr(REFIID aRIID) { + LPOLESTR str = nullptr; + HRESULT hr = ::StringFromIID(aRIID, &str); + if (FAILED(hr) || !str || !str[0]) { + return ""_ns; + } + + nsAutoString key(L"Interface\\"); + key += str; + + nsCString result; + wchar_t buf[256]; + if (WinRegistry::GetString(HKEY_CLASSES_ROOT, key, u""_ns, buf, + WinRegistry::kLegacyWinUtilsStringFlags)) { + result = NS_ConvertUTF16toUTF8(buf); + } else { + result = NS_ConvertUTF16toUTF8(str); + } + + ::CoTaskMemFree(str); + return result; +} + +static const char* GetCommonReturnValueName(HRESULT aResult) { + switch (aResult) { + case S_OK: + return "S_OK"; + case E_ABORT: + return "E_ABORT"; + case E_ACCESSDENIED: + return "E_ACCESSDENIED"; + case E_FAIL: + return "E_FAIL"; + case E_HANDLE: + return "E_HANDLE"; + case E_INVALIDARG: + return "E_INVALIDARG"; + case E_NOINTERFACE: + return "E_NOINTERFACE"; + case E_NOTIMPL: + return "E_NOTIMPL"; + case E_OUTOFMEMORY: + return "E_OUTOFMEMORY"; + case E_POINTER: + return "E_POINTER"; + case E_UNEXPECTED: + return "E_UNEXPECTED"; + default: + return SUCCEEDED(aResult) ? "Succeeded" : "Failed"; + } +} + +static const char* GetTextStoreReturnValueName(HRESULT aResult) { + switch (aResult) { + case TS_E_FORMAT: + return "TS_E_FORMAT"; + case TS_E_INVALIDPOINT: + return "TS_E_INVALIDPOINT"; + case TS_E_INVALIDPOS: + return "TS_E_INVALIDPOS"; + case TS_E_NOINTERFACE: + return "TS_E_NOINTERFACE"; + case TS_E_NOLAYOUT: + return "TS_E_NOLAYOUT"; + case TS_E_NOLOCK: + return "TS_E_NOLOCK"; + case TS_E_NOOBJECT: + return "TS_E_NOOBJECT"; + case TS_E_NOSELECTION: + return "TS_E_NOSELECTION"; + case TS_E_NOSERVICE: + return "TS_E_NOSERVICE"; + case TS_E_READONLY: + return "TS_E_READONLY"; + case TS_E_SYNCHRONOUS: + return "TS_E_SYNCHRONOUS"; + case TS_S_ASYNC: + return "TS_S_ASYNC"; + default: + return GetCommonReturnValueName(aResult); + } +} + +static const nsCString GetSinkMaskNameStr(DWORD aSinkMask) { + nsCString description; + if (aSinkMask & TS_AS_TEXT_CHANGE) { + description.AppendLiteral("TS_AS_TEXT_CHANGE"); + } + if (aSinkMask & TS_AS_SEL_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_SEL_CHANGE"); + } + if (aSinkMask & TS_AS_LAYOUT_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_LAYOUT_CHANGE"); + } + if (aSinkMask & TS_AS_ATTR_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_ATTR_CHANGE"); + } + if (aSinkMask & TS_AS_STATUS_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_STATUS_CHANGE"); + } + if (description.IsEmpty()) { + description.AppendLiteral("not-specified"); + } + return description; +} + +static const nsCString GetLockFlagNameStr(DWORD aLockFlags) { + nsCString description; + if ((aLockFlags & TS_LF_READWRITE) == TS_LF_READWRITE) { + description.AppendLiteral("TS_LF_READWRITE"); + } else if (aLockFlags & TS_LF_READ) { + description.AppendLiteral("TS_LF_READ"); + } + if (aLockFlags & TS_LF_SYNC) { + if (!description.IsEmpty()) { + description.AppendLiteral(" | "); + } + description.AppendLiteral("TS_LF_SYNC"); + } + if (description.IsEmpty()) { + description.AppendLiteral("not-specified"); + } + return description; +} + +static const char* GetTextRunTypeName(TsRunType aRunType) { + switch (aRunType) { + case TS_RT_PLAIN: + return "TS_RT_PLAIN"; + case TS_RT_HIDDEN: + return "TS_RT_HIDDEN"; + case TS_RT_OPAQUE: + return "TS_RT_OPAQUE"; + default: + return "Unknown"; + } +} + +static nsCString GetColorName(const TF_DA_COLOR& aColor) { + switch (aColor.type) { + case TF_CT_NONE: + return "TF_CT_NONE"_ns; + case TF_CT_SYSCOLOR: + return nsPrintfCString("TF_CT_SYSCOLOR, nIndex:0x%08X", + static_cast<int32_t>(aColor.nIndex)); + case TF_CT_COLORREF: + return nsPrintfCString("TF_CT_COLORREF, cr:0x%08X", + static_cast<int32_t>(aColor.cr)); + break; + default: + return nsPrintfCString("Unknown(%08X)", + static_cast<int32_t>(aColor.type)); + } +} + +static nsCString GetLineStyleName(TF_DA_LINESTYLE aLineStyle) { + switch (aLineStyle) { + case TF_LS_NONE: + return "TF_LS_NONE"_ns; + case TF_LS_SOLID: + return "TF_LS_SOLID"_ns; + case TF_LS_DOT: + return "TF_LS_DOT"_ns; + case TF_LS_DASH: + return "TF_LS_DASH"_ns; + case TF_LS_SQUIGGLE: + return "TF_LS_SQUIGGLE"_ns; + default: { + return nsPrintfCString("Unknown(%08X)", static_cast<int32_t>(aLineStyle)); + } + } +} + +static nsCString GetClauseAttrName(TF_DA_ATTR_INFO aAttr) { + switch (aAttr) { + case TF_ATTR_INPUT: + return "TF_ATTR_INPUT"_ns; + case TF_ATTR_TARGET_CONVERTED: + return "TF_ATTR_TARGET_CONVERTED"_ns; + case TF_ATTR_CONVERTED: + return "TF_ATTR_CONVERTED"_ns; + case TF_ATTR_TARGET_NOTCONVERTED: + return "TF_ATTR_TARGET_NOTCONVERTED"_ns; + case TF_ATTR_INPUT_ERROR: + return "TF_ATTR_INPUT_ERROR"_ns; + case TF_ATTR_FIXEDCONVERTED: + return "TF_ATTR_FIXEDCONVERTED"_ns; + case TF_ATTR_OTHER: + return "TF_ATTR_OTHER"_ns; + default: { + return nsPrintfCString("Unknown(%08X)", static_cast<int32_t>(aAttr)); + } + } +} + +static nsCString GetDisplayAttrStr(const TF_DISPLAYATTRIBUTE& aDispAttr) { + nsCString str; + str = "crText:{ "; + str += GetColorName(aDispAttr.crText); + str += " }, crBk:{ "; + str += GetColorName(aDispAttr.crBk); + str += " }, lsStyle: "; + str += GetLineStyleName(aDispAttr.lsStyle); + str += ", fBoldLine: "; + str += GetBoolName(aDispAttr.fBoldLine); + str += ", crLine:{ "; + str += GetColorName(aDispAttr.crLine); + str += " }, bAttr: "; + str += GetClauseAttrName(aDispAttr.bAttr); + return str; +} + +static const char* GetMouseButtonName(int16_t aButton) { + switch (aButton) { + case MouseButton::ePrimary: + return "LeftButton"; + case MouseButton::eMiddle: + return "MiddleButton"; + case MouseButton::eSecondary: + return "RightButton"; + default: + return "UnknownButton"; + } +} + +#define ADD_SEPARATOR_IF_NECESSARY(aStr) \ + if (!aStr.IsEmpty()) { \ + aStr.AppendLiteral(", "); \ + } + +static nsCString GetMouseButtonsName(int16_t aButtons) { + if (!aButtons) { + return "no buttons"_ns; + } + nsCString names; + if (aButtons & MouseButtonsFlag::ePrimaryFlag) { + names = "LeftButton"; + } + if (aButtons & MouseButtonsFlag::eSecondaryFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "RightButton"; + } + if (aButtons & MouseButtonsFlag::eMiddleFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "MiddleButton"; + } + if (aButtons & MouseButtonsFlag::e4thFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "4thButton"; + } + if (aButtons & MouseButtonsFlag::e5thFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "5thButton"; + } + return names; +} + +static nsCString GetModifiersName(Modifiers aModifiers) { + if (aModifiers == MODIFIER_NONE) { + return "no modifiers"_ns; + } + nsCString names; + if (aModifiers & MODIFIER_ALT) { + names = NS_DOM_KEYNAME_ALT; + } + if (aModifiers & MODIFIER_ALTGRAPH) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_ALTGRAPH; + } + if (aModifiers & MODIFIER_CAPSLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_CAPSLOCK; + } + if (aModifiers & MODIFIER_CONTROL) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_CONTROL; + } + if (aModifiers & MODIFIER_FN) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_FN; + } + if (aModifiers & MODIFIER_FNLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_FNLOCK; + } + if (aModifiers & MODIFIER_META) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_META; + } + if (aModifiers & MODIFIER_NUMLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_NUMLOCK; + } + if (aModifiers & MODIFIER_SCROLLLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SCROLLLOCK; + } + if (aModifiers & MODIFIER_SHIFT) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SHIFT; + } + if (aModifiers & MODIFIER_SYMBOL) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SYMBOL; + } + if (aModifiers & MODIFIER_SYMBOLLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SYMBOLLOCK; + } + return names; +} + +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 GetEscapedUTF8String final : public NS_ConvertUTF16toUTF8 { + public: + explicit GetEscapedUTF8String(const nsAString& aString) + : NS_ConvertUTF16toUTF8(aString) { + Escape(); + } + explicit GetEscapedUTF8String(const char16ptr_t aString) + : NS_ConvertUTF16toUTF8(aString) { + Escape(); + } + GetEscapedUTF8String(const char16ptr_t aString, uint32_t aLength) + : NS_ConvertUTF16toUTF8(aString, aLength) { + Escape(); + } + + private: + void Escape() { + ReplaceSubstring("\r", "\\r"); + ReplaceSubstring("\n", "\\n"); + ReplaceSubstring("\t", "\\t"); + } +}; + +class GetInputScopeString : public nsAutoCString { + public: + explicit GetInputScopeString(const nsTArray<InputScope>& aList) { + for (InputScope inputScope : aList) { + if (!IsEmpty()) { + AppendLiteral(", "); + } + switch (inputScope) { + case IS_DEFAULT: + AppendLiteral("IS_DEFAULT"); + break; + case IS_URL: + AppendLiteral("IS_URL"); + break; + case IS_FILE_FULLFILEPATH: + AppendLiteral("IS_FILE_FULLFILEPATH"); + break; + case IS_FILE_FILENAME: + AppendLiteral("IS_FILE_FILENAME"); + break; + case IS_EMAIL_USERNAME: + AppendLiteral("IS_EMAIL_USERNAME"); + break; + case IS_EMAIL_SMTPEMAILADDRESS: + AppendLiteral("IS_EMAIL_SMTPEMAILADDRESS"); + break; + case IS_LOGINNAME: + AppendLiteral("IS_LOGINNAME"); + break; + case IS_PERSONALNAME_FULLNAME: + AppendLiteral("IS_PERSONALNAME_FULLNAME"); + break; + case IS_PERSONALNAME_PREFIX: + AppendLiteral("IS_PERSONALNAME_PREFIX"); + break; + case IS_PERSONALNAME_GIVENNAME: + AppendLiteral("IS_PERSONALNAME_GIVENNAME"); + break; + case IS_PERSONALNAME_MIDDLENAME: + AppendLiteral("IS_PERSONALNAME_MIDDLENAME"); + break; + case IS_PERSONALNAME_SURNAME: + AppendLiteral("IS_PERSONALNAME_SURNAME"); + break; + case IS_PERSONALNAME_SUFFIX: + AppendLiteral("IS_PERSONALNAME_SUFFIX"); + break; + case IS_ADDRESS_FULLPOSTALADDRESS: + AppendLiteral("IS_ADDRESS_FULLPOSTALADDRESS"); + break; + case IS_ADDRESS_POSTALCODE: + AppendLiteral("IS_ADDRESS_POSTALCODE"); + break; + case IS_ADDRESS_STREET: + AppendLiteral("IS_ADDRESS_STREET"); + break; + case IS_ADDRESS_STATEORPROVINCE: + AppendLiteral("IS_ADDRESS_STATEORPROVINCE"); + break; + case IS_ADDRESS_CITY: + AppendLiteral("IS_ADDRESS_CITY"); + break; + case IS_ADDRESS_COUNTRYNAME: + AppendLiteral("IS_ADDRESS_COUNTRYNAME"); + break; + case IS_ADDRESS_COUNTRYSHORTNAME: + AppendLiteral("IS_ADDRESS_COUNTRYSHORTNAME"); + break; + case IS_CURRENCY_AMOUNTANDSYMBOL: + AppendLiteral("IS_CURRENCY_AMOUNTANDSYMBOL"); + break; + case IS_CURRENCY_AMOUNT: + AppendLiteral("IS_CURRENCY_AMOUNT"); + break; + case IS_DATE_FULLDATE: + AppendLiteral("IS_DATE_FULLDATE"); + break; + case IS_DATE_MONTH: + AppendLiteral("IS_DATE_MONTH"); + break; + case IS_DATE_DAY: + AppendLiteral("IS_DATE_DAY"); + break; + case IS_DATE_YEAR: + AppendLiteral("IS_DATE_YEAR"); + break; + case IS_DATE_MONTHNAME: + AppendLiteral("IS_DATE_MONTHNAME"); + break; + case IS_DATE_DAYNAME: + AppendLiteral("IS_DATE_DAYNAME"); + break; + case IS_DIGITS: + AppendLiteral("IS_DIGITS"); + break; + case IS_NUMBER: + AppendLiteral("IS_NUMBER"); + break; + case IS_ONECHAR: + AppendLiteral("IS_ONECHAR"); + break; + case IS_PASSWORD: + AppendLiteral("IS_PASSWORD"); + break; + case IS_TELEPHONE_FULLTELEPHONENUMBER: + AppendLiteral("IS_TELEPHONE_FULLTELEPHONENUMBER"); + break; + case IS_TELEPHONE_COUNTRYCODE: + AppendLiteral("IS_TELEPHONE_COUNTRYCODE"); + break; + case IS_TELEPHONE_AREACODE: + AppendLiteral("IS_TELEPHONE_AREACODE"); + break; + case IS_TELEPHONE_LOCALNUMBER: + AppendLiteral("IS_TELEPHONE_LOCALNUMBER"); + break; + case IS_TIME_FULLTIME: + AppendLiteral("IS_TIME_FULLTIME"); + break; + case IS_TIME_HOUR: + AppendLiteral("IS_TIME_HOUR"); + break; + case IS_TIME_MINORSEC: + AppendLiteral("IS_TIME_MINORSEC"); + break; + case IS_NUMBER_FULLWIDTH: + AppendLiteral("IS_NUMBER_FULLWIDTH"); + break; + case IS_ALPHANUMERIC_HALFWIDTH: + AppendLiteral("IS_ALPHANUMERIC_HALFWIDTH"); + break; + case IS_ALPHANUMERIC_FULLWIDTH: + AppendLiteral("IS_ALPHANUMERIC_FULLWIDTH"); + break; + case IS_CURRENCY_CHINESE: + AppendLiteral("IS_CURRENCY_CHINESE"); + break; + case IS_BOPOMOFO: + AppendLiteral("IS_BOPOMOFO"); + break; + case IS_HIRAGANA: + AppendLiteral("IS_HIRAGANA"); + break; + case IS_KATAKANA_HALFWIDTH: + AppendLiteral("IS_KATAKANA_HALFWIDTH"); + break; + case IS_KATAKANA_FULLWIDTH: + AppendLiteral("IS_KATAKANA_FULLWIDTH"); + break; + case IS_HANJA: + AppendLiteral("IS_HANJA"); + break; + case IS_PHRASELIST: + AppendLiteral("IS_PHRASELIST"); + break; + case IS_REGULAREXPRESSION: + AppendLiteral("IS_REGULAREXPRESSION"); + break; + case IS_SRGS: + AppendLiteral("IS_SRGS"); + break; + case IS_XML: + AppendLiteral("IS_XML"); + break; + case IS_PRIVATE: + AppendLiteral("IS_PRIVATE"); + break; + default: + AppendPrintf("Unknown Value(%d)", inputScope); + break; + } + } + } +}; + +/******************************************************************/ +/* InputScopeImpl */ +/******************************************************************/ + +class InputScopeImpl final : public ITfInputScope { + ~InputScopeImpl() {} + + public: + explicit InputScopeImpl(const nsTArray<InputScope>& aList) + : mInputScopes(aList.Clone()) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p InputScopeImpl(%s)", this, GetInputScopeString(aList).get())); + } + + NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(InputScopeImpl) + + STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + if ((IID_IUnknown == riid) || (IID_ITfInputScope == riid)) { + *ppv = static_cast<ITfInputScope*>(this); + } + if (*ppv) { + AddRef(); + return S_OK; + } + return E_NOINTERFACE; + } + + STDMETHODIMP GetInputScopes(InputScope** pprgInputScopes, UINT* pcCount) { + uint32_t count = (mInputScopes.IsEmpty() ? 1 : mInputScopes.Length()); + + InputScope* pScope = + (InputScope*)CoTaskMemAlloc(sizeof(InputScope) * count); + NS_ENSURE_TRUE(pScope, E_OUTOFMEMORY); + + if (mInputScopes.IsEmpty()) { + *pScope = IS_DEFAULT; + *pcCount = 1; + *pprgInputScopes = pScope; + return S_OK; + } + + *pcCount = 0; + + for (uint32_t idx = 0; idx < count; idx++) { + *(pScope + idx) = mInputScopes[idx]; + (*pcCount)++; + } + + *pprgInputScopes = pScope; + return S_OK; + } + + STDMETHODIMP GetPhrase(BSTR** ppbstrPhrases, UINT* pcCount) { + return E_NOTIMPL; + } + STDMETHODIMP GetRegularExpression(BSTR* pbstrRegExp) { return E_NOTIMPL; } + STDMETHODIMP GetSRGS(BSTR* pbstrSRGS) { return E_NOTIMPL; } + STDMETHODIMP GetXML(BSTR* pbstrXML) { return E_NOTIMPL; } + + private: + nsTArray<InputScope> mInputScopes; +}; + +/******************************************************************/ +/* TSFStaticSink */ +/******************************************************************/ + +class TSFStaticSink final : public ITfInputProcessorProfileActivationSink { + public: + static TSFStaticSink* GetInstance() { + if (!sInstance) { + RefPtr<ITfThreadMgr> threadMgr = TSFTextStore::GetThreadMgr(); + if (NS_WARN_IF(!threadMgr)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink " + "instance due to no ThreadMgr instance")); + return nullptr; + } + RefPtr<ITfInputProcessorProfiles> inputProcessorProfiles = + TSFTextStore::GetInputProcessorProfiles(); + if (NS_WARN_IF(!inputProcessorProfiles)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink " + "instance due to no InputProcessorProfiles instance")); + return nullptr; + } + RefPtr<TSFStaticSink> staticSink = new TSFStaticSink(); + if (NS_WARN_IF(!staticSink->Init(threadMgr, inputProcessorProfiles))) { + staticSink->Destroy(); + MOZ_LOG( + gIMELog, LogLevel::Error, + ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink " + "instance")); + return nullptr; + } + sInstance = staticSink.forget(); + } + return sInstance; + } + + static void Shutdown() { + if (sInstance) { + sInstance->Destroy(); + sInstance = nullptr; + } + } + + bool Init(ITfThreadMgr* aThreadMgr, + ITfInputProcessorProfiles* aInputProcessorProfiles); + STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + if (IID_IUnknown == riid || + IID_ITfInputProcessorProfileActivationSink == riid) { + *ppv = static_cast<ITfInputProcessorProfileActivationSink*>(this); + } + if (*ppv) { + AddRef(); + return S_OK; + } + return E_NOINTERFACE; + } + + NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(TSFStaticSink) + + const nsString& GetActiveTIPKeyboardDescription() const { + return mActiveTIPKeyboardDescription; + } + + static bool IsIMM_IMEActive() { + // Use IMM API until TSFStaticSink starts to work. + if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) { + return IsIMM_IME(::GetKeyboardLayout(0)); + } + return sInstance->mIsIMM_IME; + } + + static bool IsIMM_IME(HKL aHKL) { + return (::ImmGetIMEFileNameW(aHKL, nullptr, 0) > 0); + } + + static bool IsTraditionalChinese() { + EnsureInstance(); + return sInstance && sInstance->IsTraditionalChineseInternal(); + } + static bool IsSimplifiedChinese() { + EnsureInstance(); + return sInstance && sInstance->IsSimplifiedChineseInternal(); + } + static bool IsJapanese() { + EnsureInstance(); + return sInstance && sInstance->IsJapaneseInternal(); + } + static bool IsKorean() { + EnsureInstance(); + return sInstance && sInstance->IsKoreanInternal(); + } + + /** + * ActiveTIP() returns an ID for currently active TIP. + * Please note that this method is expensive due to needs a lot of GUID + * comparations if active language ID is one of CJKT. If you need to + * check TIPs for a specific language, you should check current language + * first. + */ + static TextInputProcessorID ActiveTIP() { + EnsureInstance(); + if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) { + return TextInputProcessorID::eUnknown; + } + sInstance->ComputeActiveTextInputProcessor(); + if (NS_WARN_IF(sInstance->mActiveTIP == + TextInputProcessorID::eNotComputed)) { + return TextInputProcessorID::eUnknown; + } + return sInstance->mActiveTIP; + } + + static bool GetActiveTIPNameForTelemetry(nsAString& aName) { + if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) { + return false; + } + if (sInstance->mActiveTIPGUID == GUID_NULL) { + aName.Truncate(); + aName.AppendPrintf("0x%04X", sInstance->mLangID); + return true; + } + // key should be "LocaleID|Description". Although GUID of the + // profile is unique key since description may be localized for system + // language, unfortunately, it's too long to record as key with its + // description. Therefore, we should record only the description with + // LocaleID because Microsoft IME may not include language information. + // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp + aName.Truncate(); + aName.AppendPrintf("0x%04X|", sInstance->mLangID); + nsAutoString description; + description.Assign(sInstance->mActiveTIPKeyboardDescription); + static const uint32_t kMaxDescriptionLength = 72 - aName.Length(); + if (description.Length() > kMaxDescriptionLength) { + if (NS_IS_LOW_SURROGATE(description[kMaxDescriptionLength - 1]) && + NS_IS_HIGH_SURROGATE(description[kMaxDescriptionLength - 2])) { + description.Truncate(kMaxDescriptionLength - 2); + } else { + description.Truncate(kMaxDescriptionLength - 1); + } + // U+2026 is "..." + description.Append(char16_t(0x2026)); + } + aName.Append(description); + return true; + } + + static bool IsMSChangJieOrMSQuickActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Traditional Chinese. + if (!IsTraditionalChinese()) { + return false; + } + switch (ActiveTIP()) { + case TextInputProcessorID::eMicrosoftChangJie: + case TextInputProcessorID::eMicrosoftQuick: + return true; + default: + return false; + } + } + + static bool IsMSPinyinOrMSWubiActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Simplified Chinese. + if (!IsSimplifiedChinese()) { + return false; + } + switch (ActiveTIP()) { + case TextInputProcessorID::eMicrosoftPinyin: + case TextInputProcessorID::eMicrosoftWubi: + return true; + default: + return false; + } + } + + static bool IsMSJapaneseIMEActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Japanese. + if (!IsJapanese()) { + return false; + } + return ActiveTIP() == TextInputProcessorID::eMicrosoftIMEForJapanese; + } + + static bool IsGoogleJapaneseInputActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Japanese. + if (!IsJapanese()) { + return false; + } + return ActiveTIP() == TextInputProcessorID::eGoogleJapaneseInput; + } + + static bool IsATOKActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if active TIP is + // ATOK first since it's cheaper. + return IsJapanese() && sInstance->IsATOKActiveInternal(); + } + + // Note that ATOK 2011 - 2016 refers native caret position for deciding its + // popup window position. + static bool IsATOKReferringNativeCaretActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if active TIP is + // ATOK first since it's cheaper. + if (!IsJapanese() || !sInstance->IsATOKActiveInternal()) { + return false; + } + switch (ActiveTIP()) { + case TextInputProcessorID::eATOK2011: + case TextInputProcessorID::eATOK2012: + case TextInputProcessorID::eATOK2013: + case TextInputProcessorID::eATOK2014: + case TextInputProcessorID::eATOK2015: + return true; + default: + return false; + } + } + + private: + static void EnsureInstance() { + if (!sInstance) { + RefPtr<TSFStaticSink> staticSink = GetInstance(); + Unused << staticSink; + } + } + + bool IsTraditionalChineseInternal() const { return mLangID == 0x0404; } + bool IsSimplifiedChineseInternal() const { return mLangID == 0x0804; } + bool IsJapaneseInternal() const { return mLangID == 0x0411; } + bool IsKoreanInternal() const { return mLangID == 0x0412; } + + bool IsATOKActiveInternal() { + EnsureInitActiveTIPKeyboard(); + // FYI: Name of packaged ATOK includes the release year like "ATOK 2015". + // Name of ATOK Passport (subscription) equals "ATOK". + return StringBeginsWith(mActiveTIPKeyboardDescription, u"ATOK "_ns) || + mActiveTIPKeyboardDescription.EqualsLiteral("ATOK"); + } + + void ComputeActiveTextInputProcessor() { + if (mActiveTIP != TextInputProcessorID::eNotComputed) { + return; + } + + if (mActiveTIPGUID == GUID_NULL) { + mActiveTIP = TextInputProcessorID::eNone; + return; + } + + // Comparing GUID is slow. So, we should use language information to + // reduce the comparing cost for TIP which is not we do not support + // specifically since they are always compared with all supported TIPs. + switch (mLangID) { + case 0x0404: + mActiveTIP = ComputeActiveTIPAsTraditionalChinese(); + break; + case 0x0411: + mActiveTIP = ComputeActiveTIPAsJapanese(); + break; + case 0x0412: + mActiveTIP = ComputeActiveTIPAsKorean(); + break; + case 0x0804: + mActiveTIP = ComputeActiveTIPAsSimplifiedChinese(); + break; + default: + mActiveTIP = TextInputProcessorID::eUnknown; + break; + } + // Special case for Keyman Desktop, it is available for any languages. + // Therefore, we need to check it only if we don't know the active TIP. + if (mActiveTIP != TextInputProcessorID::eUnknown) { + return; + } + + // Note that keyboard layouts for Keyman assign its GUID on install + // randomly, but CLSID is constant in any environments. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1670834#c7 + // https://github.com/keymanapp/keyman/blob/318c73a9e1d571d942837ff9964590626e5bd5aa/windows/src/engine/kmtip/globals.cpp#L37 + // {FE0420F1-38D1-4B4C-96BF-E7E20A74CFB7} + static constexpr CLSID kKeymanDesktop_CLSID = { + 0xFE0420F1, + 0x38D1, + 0x4B4C, + {0x96, 0xBF, 0xE7, 0xE2, 0x0A, 0x74, 0xCF, 0xB7}}; + if (mActiveTIPCLSID == kKeymanDesktop_CLSID) { + mActiveTIP = TextInputProcessorID::eKeymanDesktop; + } + } + + TextInputProcessorID ComputeActiveTIPAsJapanese() { + // {A76C93D9-5523-4E90-AAFA-4DB112F9AC76} (Win7, Win8.1, Win10) + static constexpr GUID kMicrosoftIMEForJapaneseGUID = { + 0xA76C93D9, + 0x5523, + 0x4E90, + {0xAA, 0xFA, 0x4D, 0xB1, 0x12, 0xF9, 0xAC, 0x76}}; + if (mActiveTIPGUID == kMicrosoftIMEForJapaneseGUID) { + return TextInputProcessorID::eMicrosoftIMEForJapanese; + } + // {54EDCC94-1524-4BB1-9FB7-7BABE4F4CA64} + static constexpr GUID kMicrosoftOfficeIME2010ForJapaneseGUID = { + 0x54EDCC94, + 0x1524, + 0x4BB1, + {0x9F, 0xB7, 0x7B, 0xAB, 0xE4, 0xF4, 0xCA, 0x64}}; + if (mActiveTIPGUID == kMicrosoftOfficeIME2010ForJapaneseGUID) { + return TextInputProcessorID::eMicrosoftOfficeIME2010ForJapanese; + } + // {773EB24E-CA1D-4B1B-B420-FA985BB0B80D} + static constexpr GUID kGoogleJapaneseInputGUID = { + 0x773EB24E, + 0xCA1D, + 0x4B1B, + {0xB4, 0x20, 0xFA, 0x98, 0x5B, 0xB0, 0xB8, 0x0D}}; + if (mActiveTIPGUID == kGoogleJapaneseInputGUID) { + return TextInputProcessorID::eGoogleJapaneseInput; + } + // {F9C24A5C-8A53-499D-9572-93B2FF582115} + static const GUID kATOK2011GUID = { + 0xF9C24A5C, + 0x8A53, + 0x499D, + {0x95, 0x72, 0x93, 0xB2, 0xFF, 0x58, 0x21, 0x15}}; + if (mActiveTIPGUID == kATOK2011GUID) { + return TextInputProcessorID::eATOK2011; + } + // {1DE01562-F445-401B-B6C3-E5B18DB79461} + static constexpr GUID kATOK2012GUID = { + 0x1DE01562, + 0xF445, + 0x401B, + {0xB6, 0xC3, 0xE5, 0xB1, 0x8D, 0xB7, 0x94, 0x61}}; + if (mActiveTIPGUID == kATOK2012GUID) { + return TextInputProcessorID::eATOK2012; + } + // {3C4DB511-189A-4168-B6EA-BFD0B4C85615} + static constexpr GUID kATOK2013GUID = { + 0x3C4DB511, + 0x189A, + 0x4168, + {0xB6, 0xEA, 0xBF, 0xD0, 0xB4, 0xC8, 0x56, 0x15}}; + if (mActiveTIPGUID == kATOK2013GUID) { + return TextInputProcessorID::eATOK2013; + } + // {4EF33B79-6AA9-4271-B4BF-9321C279381B} + static constexpr GUID kATOK2014GUID = { + 0x4EF33B79, + 0x6AA9, + 0x4271, + {0xB4, 0xBF, 0x93, 0x21, 0xC2, 0x79, 0x38, 0x1B}}; + if (mActiveTIPGUID == kATOK2014GUID) { + return TextInputProcessorID::eATOK2014; + } + // {EAB4DC00-CE2E-483D-A86A-E6B99DA9599A} + static constexpr GUID kATOK2015GUID = { + 0xEAB4DC00, + 0xCE2E, + 0x483D, + {0xA8, 0x6A, 0xE6, 0xB9, 0x9D, 0xA9, 0x59, 0x9A}}; + if (mActiveTIPGUID == kATOK2015GUID) { + return TextInputProcessorID::eATOK2015; + } + // {0B557B4C-5740-4110-A60A-1493FA10BF2B} + static constexpr GUID kATOK2016GUID = { + 0x0B557B4C, + 0x5740, + 0x4110, + {0xA6, 0x0A, 0x14, 0x93, 0xFA, 0x10, 0xBF, 0x2B}}; + if (mActiveTIPGUID == kATOK2016GUID) { + return TextInputProcessorID::eATOK2016; + } + + // * ATOK 2017 + // - {6DBFD8F5-701D-11E6-920F-782BCBA6348F} + // * ATOK Passport (confirmed with version 31.1.2) + // - {A38F2FD9-7199-45E1-841C-BE0313D8052F} + + if (IsATOKActiveInternal()) { + return TextInputProcessorID::eATOKUnknown; + } + + // {E6D66705-1EDA-4373-8D01-1D0CB2D054C7} + static constexpr GUID kJapanist10GUID = { + 0xE6D66705, + 0x1EDA, + 0x4373, + {0x8D, 0x01, 0x1D, 0x0C, 0xB2, 0xD0, 0x54, 0xC7}}; + if (mActiveTIPGUID == kJapanist10GUID) { + return TextInputProcessorID::eJapanist10; + } + + return TextInputProcessorID::eUnknown; + } + + TextInputProcessorID ComputeActiveTIPAsTraditionalChinese() { + // {B2F9C502-1742-11D4-9790-0080C882687E} (Win8.1, Win10) + static constexpr GUID kMicrosoftBopomofoGUID = { + 0xB2F9C502, + 0x1742, + 0x11D4, + {0x97, 0x90, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftBopomofoGUID) { + return TextInputProcessorID::eMicrosoftBopomofo; + } + // {4BDF9F03-C7D3-11D4-B2AB-0080C882687E} (Win7, Win8.1, Win10) + static const GUID kMicrosoftChangJieGUID = { + 0x4BDF9F03, + 0xC7D3, + 0x11D4, + {0xB2, 0xAB, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftChangJieGUID) { + return TextInputProcessorID::eMicrosoftChangJie; + } + // {761309DE-317A-11D4-9B5D-0080C882687E} (Win7) + static constexpr GUID kMicrosoftPhoneticGUID = { + 0x761309DE, + 0x317A, + 0x11D4, + {0x9B, 0x5D, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftPhoneticGUID) { + return TextInputProcessorID::eMicrosoftPhonetic; + } + // {6024B45F-5C54-11D4-B921-0080C882687E} (Win7, Win8.1, Win10) + static constexpr GUID kMicrosoftQuickGUID = { + 0x6024B45F, + 0x5C54, + 0x11D4, + {0xB9, 0x21, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftQuickGUID) { + return TextInputProcessorID::eMicrosoftQuick; + } + // {F3BA907A-6C7E-11D4-97FA-0080C882687E} (Win7) + static constexpr GUID kMicrosoftNewChangJieGUID = { + 0xF3BA907A, + 0x6C7E, + 0x11D4, + {0x97, 0xFA, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftNewChangJieGUID) { + return TextInputProcessorID::eMicrosoftNewChangJie; + } + // {B2F9C502-1742-11D4-9790-0080C882687E} (Win7) + static constexpr GUID kMicrosoftNewPhoneticGUID = { + 0xB2F9C502, + 0x1742, + 0x11D4, + {0x97, 0x90, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftNewPhoneticGUID) { + return TextInputProcessorID::eMicrosoftNewPhonetic; + } + // {0B883BA0-C1C7-11D4-87F9-0080C882687E} (Win7) + static constexpr GUID kMicrosoftNewQuickGUID = { + 0x0B883BA0, + 0xC1C7, + 0x11D4, + {0x87, 0xF9, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftNewQuickGUID) { + return TextInputProcessorID::eMicrosoftNewQuick; + } + + // NOTE: There are some other Traditional Chinese TIPs installed in Windows: + // * Chinese Traditional Array (version 6.0) + // - {D38EFF65-AA46-4FD5-91A7-67845FB02F5B} (Win7, Win8.1) + // * Chinese Traditional DaYi (version 6.0) + // - {037B2C25-480C-4D7F-B027-D6CA6B69788A} (Win7, Win8.1) + + // {B58630B5-0ED3-4335-BBC9-E77BBCB43CAD} + static const GUID kFreeChangJieGUID = { + 0xB58630B5, + 0x0ED3, + 0x4335, + {0xBB, 0xC9, 0xE7, 0x7B, 0xBC, 0xB4, 0x3C, 0xAD}}; + if (mActiveTIPGUID == kFreeChangJieGUID) { + return TextInputProcessorID::eFreeChangJie; + } + + return TextInputProcessorID::eUnknown; + } + + TextInputProcessorID ComputeActiveTIPAsSimplifiedChinese() { + // FYI: This matches with neither "Microsoft Pinyin ABC Input Style" nor + // "Microsoft Pinyin New Experience Input Style" on Win7. + // {FA550B04-5AD7-411F-A5AC-CA038EC515D7} (Win8.1, Win10) + static constexpr GUID kMicrosoftPinyinGUID = { + 0xFA550B04, + 0x5AD7, + 0x411F, + {0xA5, 0xAC, 0xCA, 0x03, 0x8E, 0xC5, 0x15, 0xD7}}; + if (mActiveTIPGUID == kMicrosoftPinyinGUID) { + return TextInputProcessorID::eMicrosoftPinyin; + } + + // {F3BA9077-6C7E-11D4-97FA-0080C882687E} (Win7) + static constexpr GUID kMicrosoftPinyinNewExperienceInputStyleGUID = { + 0xF3BA9077, + 0x6C7E, + 0x11D4, + {0x97, 0xFA, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftPinyinNewExperienceInputStyleGUID) { + return TextInputProcessorID::eMicrosoftPinyinNewExperienceInputStyle; + } + // {82590C13-F4DD-44F4-BA1D-8667246FDF8E} (Win8.1, Win10) + static constexpr GUID kMicrosoftWubiGUID = { + 0x82590C13, + 0xF4DD, + 0x44F4, + {0xBA, 0x1D, 0x86, 0x67, 0x24, 0x6F, 0xDF, 0x8E}}; + if (mActiveTIPGUID == kMicrosoftWubiGUID) { + return TextInputProcessorID::eMicrosoftWubi; + } + // NOTE: There are some other Simplified Chinese TIPs installed in Windows: + // * Chinese Simplified QuanPin (version 6.0) + // - {54FC610E-6ABD-4685-9DDD-A130BDF1B170} (Win8.1) + // * Chinese Simplified ZhengMa (version 6.0) + // - {733B4D81-3BC3-4132-B91A-E9CDD5E2BFC9} (Win8.1) + // * Chinese Simplified ShuangPin (version 6.0) + // - {EF63706D-31C4-490E-9DBB-BD150ADC454B} (Win8.1) + // * Microsoft Pinyin ABC Input Style + // - {FCA121D2-8C6D-41FB-B2DE-A2AD110D4820} (Win7) + return TextInputProcessorID::eUnknown; + } + + TextInputProcessorID ComputeActiveTIPAsKorean() { + // {B5FE1F02-D5F2-4445-9C03-C568F23C99A1} (Win7, Win8.1, Win10) + static constexpr GUID kMicrosoftIMEForKoreanGUID = { + 0xB5FE1F02, + 0xD5F2, + 0x4445, + {0x9C, 0x03, 0xC5, 0x68, 0xF2, 0x3C, 0x99, 0xA1}}; + if (mActiveTIPGUID == kMicrosoftIMEForKoreanGUID) { + return TextInputProcessorID::eMicrosoftIMEForKorean; + } + // {B60AF051-257A-46BC-B9D3-84DAD819BAFB} (Win8.1, Win10) + static constexpr GUID kMicrosoftOldHangulGUID = { + 0xB60AF051, + 0x257A, + 0x46BC, + {0xB9, 0xD3, 0x84, 0xDA, 0xD8, 0x19, 0xBA, 0xFB}}; + if (mActiveTIPGUID == kMicrosoftOldHangulGUID) { + return TextInputProcessorID::eMicrosoftOldHangul; + } + + // NOTE: There is the other Korean TIP installed in Windows: + // * Microsoft IME 2010 + // - {48878C45-93F9-4aaf-A6A1-272CD863C4F5} (Win7) + + return TextInputProcessorID::eUnknown; + } + + public: // ITfInputProcessorProfileActivationSink + STDMETHODIMP OnActivated(DWORD, LANGID, REFCLSID, REFGUID, REFGUID, HKL, + DWORD); + + private: + TSFStaticSink(); + virtual ~TSFStaticSink() {} + + bool EnsureInitActiveTIPKeyboard(); + + void Destroy(); + + void GetTIPDescription(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile, nsAString& aDescription); + bool IsTIPCategoryKeyboard(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile); + + TextInputProcessorID mActiveTIP; + + // Cookie of installing ITfInputProcessorProfileActivationSink + DWORD mIPProfileCookie; + + LANGID mLangID; + + // True if current IME is implemented with IMM. + bool mIsIMM_IME; + // True if OnActivated() is already called + bool mOnActivatedCalled; + + RefPtr<ITfThreadMgr> mThreadMgr; + RefPtr<ITfInputProcessorProfiles> mInputProcessorProfiles; + + // Active TIP keyboard's description. If active language profile isn't TIP, + // i.e., IMM-IME or just a keyboard layout, this is empty. + nsString mActiveTIPKeyboardDescription; + + // Active TIP's GUID and CLSID + GUID mActiveTIPGUID; + CLSID mActiveTIPCLSID; + + static StaticRefPtr<TSFStaticSink> sInstance; +}; + +StaticRefPtr<TSFStaticSink> TSFStaticSink::sInstance; + +TSFStaticSink::TSFStaticSink() + : mActiveTIP(TextInputProcessorID::eNotComputed), + mIPProfileCookie(TF_INVALID_COOKIE), + mLangID(0), + mIsIMM_IME(false), + mOnActivatedCalled(false), + mActiveTIPGUID(GUID_NULL) {} + +bool TSFStaticSink::Init(ITfThreadMgr* aThreadMgr, + ITfInputProcessorProfiles* aInputProcessorProfiles) { + MOZ_ASSERT(!mThreadMgr && !mInputProcessorProfiles, + "TSFStaticSink::Init() must be called only once"); + + mThreadMgr = aThreadMgr; + mInputProcessorProfiles = aInputProcessorProfiles; + + RefPtr<ITfSource> source; + HRESULT hr = + mThreadMgr->QueryInterface(IID_ITfSource, getter_AddRefs(source)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::Init() FAILED to get ITfSource " + "instance (0x%08lX)", + this, hr)); + return false; + } + + // NOTE: On Vista or later, Windows let us know activate IME changed only + // with ITfInputProcessorProfileActivationSink. + hr = source->AdviseSink( + IID_ITfInputProcessorProfileActivationSink, + static_cast<ITfInputProcessorProfileActivationSink*>(this), + &mIPProfileCookie); + if (FAILED(hr) || mIPProfileCookie == TF_INVALID_COOKIE) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::Init() FAILED to install " + "ITfInputProcessorProfileActivationSink (0x%08lX)", + this, hr)); + return false; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::Init(), " + "mIPProfileCookie=0x%08lX", + this, mIPProfileCookie)); + return true; +} + +void TSFStaticSink::Destroy() { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::Shutdown() " + "mIPProfileCookie=0x%08lX", + this, mIPProfileCookie)); + + if (mIPProfileCookie != TF_INVALID_COOKIE) { + RefPtr<ITfSource> source; + HRESULT hr = + mThreadMgr->QueryInterface(IID_ITfSource, getter_AddRefs(source)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::Shutdown() FAILED to get " + "ITfSource instance (0x%08lX)", + this, hr)); + } else { + hr = source->UnadviseSink(mIPProfileCookie); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Shutdown() FAILED to uninstall " + "ITfInputProcessorProfileActivationSink (0x%08lX)", + this, hr)); + } + } + } + + mThreadMgr = nullptr; + mInputProcessorProfiles = nullptr; +} + +STDMETHODIMP +TSFStaticSink::OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID rclsid, + REFGUID catid, REFGUID guidProfile, HKL hkl, + DWORD dwFlags) { + if ((dwFlags & TF_IPSINK_FLAG_ACTIVE) && + (dwProfileType == TF_PROFILETYPE_KEYBOARDLAYOUT || + catid == GUID_TFCAT_TIP_KEYBOARD)) { + mOnActivatedCalled = true; + mActiveTIP = TextInputProcessorID::eNotComputed; + mActiveTIPGUID = guidProfile; + mActiveTIPCLSID = rclsid; + mLangID = langid & 0xFFFF; + mIsIMM_IME = IsIMM_IME(hkl); + GetTIPDescription(rclsid, langid, guidProfile, + mActiveTIPKeyboardDescription); + if (mActiveTIPGUID != GUID_NULL) { + // key should be "LocaleID|Description". Although GUID of the + // profile is unique key since description may be localized for system + // language, unfortunately, it's too long to record as key with its + // description. Therefore, we should record only the description with + // LocaleID because Microsoft IME may not include language information. + // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp + nsAutoString key; + TSFStaticSink::GetActiveTIPNameForTelemetry(key); + Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_WINDOWS, key, + true); + } + // Notify IMEHandler of changing active keyboard layout. + IMEHandler::OnKeyboardLayoutChanged(); + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::OnActivated(dwProfileType=%s (0x%08lX), " + "langid=0x%08X, rclsid=%s, catid=%s, guidProfile=%s, hkl=0x%p, " + "dwFlags=0x%08lX (TF_IPSINK_FLAG_ACTIVE: %s)), mIsIMM_IME=%s, " + "mActiveTIPDescription=\"%s\"", + this, + dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR + ? "TF_PROFILETYPE_INPUTPROCESSOR" + : dwProfileType == TF_PROFILETYPE_KEYBOARDLAYOUT + ? "TF_PROFILETYPE_KEYBOARDLAYOUT" + : "Unknown", + dwProfileType, langid, GetCLSIDNameStr(rclsid).get(), + GetGUIDNameStr(catid).get(), GetGUIDNameStr(guidProfile).get(), hkl, + dwFlags, GetBoolName(dwFlags & TF_IPSINK_FLAG_ACTIVE), + GetBoolName(mIsIMM_IME), + NS_ConvertUTF16toUTF8(mActiveTIPKeyboardDescription).get())); + return S_OK; +} + +bool TSFStaticSink::EnsureInitActiveTIPKeyboard() { + if (mOnActivatedCalled) { + return true; + } + + RefPtr<ITfInputProcessorProfileMgr> profileMgr; + HRESULT hr = mInputProcessorProfiles->QueryInterface( + IID_ITfInputProcessorProfileMgr, getter_AddRefs(profileMgr)); + if (FAILED(hr) || !profileMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get input processor profile manager, hr=0x%08lX", + this, hr)); + return false; + } + + TF_INPUTPROCESSORPROFILE profile; + hr = profileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile); + if (hr == S_FALSE) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get active keyboard layout profile due to no active profile, " + "hr=0x%08lX", + this, hr)); + // XXX Should we call OnActivated() with arguments like non-TIP in this + // case? + return false; + } + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get active TIP keyboard, hr=0x%08lX", + this, hr)); + return false; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), " + "calling OnActivated() manually...", + this)); + OnActivated(profile.dwProfileType, profile.langid, profile.clsid, + profile.catid, profile.guidProfile, ::GetKeyboardLayout(0), + TF_IPSINK_FLAG_ACTIVE); + return true; +} + +void TSFStaticSink::GetTIPDescription(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile, + nsAString& aDescription) { + aDescription.Truncate(); + + if (aTextService == CLSID_NULL || aProfile == GUID_NULL) { + return; + } + + BSTR description = nullptr; + HRESULT hr = mInputProcessorProfiles->GetLanguageProfileDescription( + aTextService, aLangID, aProfile, &description); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::InitActiveTIPDescription() FAILED " + "due to GetLanguageProfileDescription() failure, hr=0x%08lX", + this, hr)); + return; + } + + if (description && description[0]) { + aDescription.Assign(description); + } + ::SysFreeString(description); +} + +bool TSFStaticSink::IsTIPCategoryKeyboard(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile) { + if (aTextService == CLSID_NULL || aProfile == GUID_NULL) { + return false; + } + + RefPtr<IEnumTfLanguageProfiles> enumLangProfiles; + HRESULT hr = mInputProcessorProfiles->EnumLanguageProfiles( + aLangID, getter_AddRefs(enumLangProfiles)); + if (FAILED(hr) || !enumLangProfiles) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::IsTIPCategoryKeyboard(), FAILED " + "to get language profiles enumerator, hr=0x%08lX", + this, hr)); + return false; + } + + TF_LANGUAGEPROFILE profile; + ULONG fetch = 0; + while (SUCCEEDED(enumLangProfiles->Next(1, &profile, &fetch)) && fetch) { + // XXX We're not sure a profile is registered with two or more categories. + if (profile.clsid == aTextService && profile.guidProfile == aProfile && + profile.catid == GUID_TFCAT_TIP_KEYBOARD) { + return true; + } + } + return false; +} + +/******************************************************************/ +/* TSFTextStore */ +/******************************************************************/ + +StaticRefPtr<ITfThreadMgr> TSFTextStore::sThreadMgr; +StaticRefPtr<ITfMessagePump> TSFTextStore::sMessagePump; +StaticRefPtr<ITfKeystrokeMgr> TSFTextStore::sKeystrokeMgr; +StaticRefPtr<ITfDisplayAttributeMgr> TSFTextStore::sDisplayAttrMgr; +StaticRefPtr<ITfCategoryMgr> TSFTextStore::sCategoryMgr; +StaticRefPtr<ITfCompartment> TSFTextStore::sCompartmentForOpenClose; +StaticRefPtr<ITfDocumentMgr> TSFTextStore::sDisabledDocumentMgr; +StaticRefPtr<ITfContext> TSFTextStore::sDisabledContext; +StaticRefPtr<ITfInputProcessorProfiles> TSFTextStore::sInputProcessorProfiles; +StaticRefPtr<TSFTextStore> TSFTextStore::sEnabledTextStore; +const MSG* TSFTextStore::sHandlingKeyMsg = nullptr; +DWORD TSFTextStore::sClientId = 0; +bool TSFTextStore::sIsKeyboardEventDispatched = false; + +#define TEXTSTORE_DEFAULT_VIEW (1) + +TSFTextStore::TSFTextStore() + : mEditCookie(0), + mSinkMask(0), + mLock(0), + mLockQueued(0), + mHandlingKeyMessage(0) { + // We hope that 5 or more actions don't occur at once. + mPendingActions.SetCapacity(5); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::TSFTextStore() SUCCEEDED", this)); +} + +TSFTextStore::~TSFTextStore() { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore instance is destroyed", this)); +} + +bool TSFTextStore::Init(nsWindow* aWidget, const InputContext& aContext) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Init(aWidget=0x%p)", this, aWidget)); + + if (NS_WARN_IF(!aWidget) || NS_WARN_IF(aWidget->Destroyed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED due to being initialized with " + "destroyed widget", + this)); + return false; + } + + if (mDocumentMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED due to already initialized", + this)); + return false; + } + + mWidget = aWidget; + if (NS_WARN_IF(!mWidget)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED " + "due to aWidget is nullptr ", + this)); + return false; + } + mDispatcher = mWidget->GetTextEventDispatcher(); + if (NS_WARN_IF(!mDispatcher)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED " + "due to aWidget->GetTextEventDispatcher() failure", + this)); + return false; + } + + mInPrivateBrowsing = aContext.mInPrivateBrowsing; + SetInputScope(aContext.mHTMLInputType, aContext.mHTMLInputMode); + + if (aContext.mURI) { + // We don't need the document URL if it fails, let's ignore the error. + nsAutoCString spec; + if (NS_SUCCEEDED(aContext.mURI->GetSpec(spec))) { + CopyUTF8toUTF16(spec, mDocumentURL); + } + } + + // Create document manager + RefPtr<ITfThreadMgr> threadMgr = sThreadMgr; + RefPtr<ITfDocumentMgr> documentMgr; + HRESULT hr = threadMgr->CreateDocumentMgr(getter_AddRefs(documentMgr)); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfDocumentMgr " + "(0x%08lX)", + this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfDocumentMgr due to " + "TextStore being destroyed during calling " + "ITfThreadMgr::CreateDocumentMgr()", + this)); + return false; + } + // Create context and add it to document manager + RefPtr<ITfContext> context; + hr = documentMgr->CreateContext(sClientId, 0, + static_cast<ITextStoreACP*>(this), + getter_AddRefs(context), &mEditCookie); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create the context " + "(0x%08lX)", + this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfContext due to " + "TextStore being destroyed during calling " + "ITfDocumentMgr::CreateContext()", + this)); + return false; + } + + hr = documentMgr->Push(context); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to push the context (0x%08lX)", + this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfContext due to " + "TextStore being destroyed during calling ITfDocumentMgr::Push()", + this)); + documentMgr->Pop(TF_POPF_ALL); + return false; + } + + mDocumentMgr = documentMgr; + mContext = context; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Init() succeeded: " + "mDocumentMgr=0x%p, mContext=0x%p, mEditCookie=0x%08lX", + this, mDocumentMgr.get(), mContext.get(), mEditCookie)); + + return true; +} + +void TSFTextStore::Destroy() { + if (mBeingDestroyed) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Destroy(), mLock=%s, " + "mComposition=%s, mHandlingKeyMessage=%u", + this, GetLockFlagNameStr(mLock).get(), + ToString(mComposition).c_str(), mHandlingKeyMessage)); + + mDestroyed = true; + + // Destroy native caret first because it's not directly related to TSF and + // there may be another textstore which gets focus. So, we should avoid + // to destroy caret after the new one recreates caret. + IMEHandler::MaybeDestroyNativeCaret(); + + if (mLock) { + mPendingDestroy = true; + return; + } + + AutoRestore<bool> savedBeingDestroyed(mBeingDestroyed); + mBeingDestroyed = true; + + // If there is composition, TSF keeps the composition even after the text + // store destroyed. So, we should clear the composition here. + if (mComposition.isSome()) { + CommitCompositionInternal(false); + } + + if (mSink) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::Destroy(), calling " + "ITextStoreACPSink::OnLayoutChange(TS_LC_DESTROY)...", + this)); + RefPtr<ITextStoreACPSink> sink = mSink; + sink->OnLayoutChange(TS_LC_DESTROY, TEXTSTORE_DEFAULT_VIEW); + } + + // If this is called during handling a keydown or keyup message, we should + // put off to release TSF objects until it completely finishes since + // MS-IME for Japanese refers some objects without grabbing them. + if (!mHandlingKeyMessage) { + ReleaseTSFObjects(); + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Destroy() succeeded", this)); +} + +void TSFTextStore::ReleaseTSFObjects() { + MOZ_ASSERT(!mHandlingKeyMessage); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::ReleaseTSFObjects()", this)); + + mDocumentURL.Truncate(); + mContext = nullptr; + if (mDocumentMgr) { + RefPtr<ITfDocumentMgr> documentMgr = mDocumentMgr.forget(); + documentMgr->Pop(TF_POPF_ALL); + } + mSink = nullptr; + mWidget = nullptr; + mDispatcher = nullptr; + + if (!mMouseTrackers.IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::ReleaseTSFObjects(), " + "removing a mouse tracker...", + this)); + mMouseTrackers.Clear(); + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::ReleaseTSFObjects() completed", this)); +} + +STDMETHODIMP +TSFTextStore::QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + if ((IID_IUnknown == riid) || (IID_ITextStoreACP == riid)) { + *ppv = static_cast<ITextStoreACP*>(this); + } else if (IID_ITfContextOwnerCompositionSink == riid) { + *ppv = static_cast<ITfContextOwnerCompositionSink*>(this); + } else if (IID_ITfMouseTrackerACP == riid) { + *ppv = static_cast<ITfMouseTrackerACP*>(this); + } + if (*ppv) { + AddRef(); + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::QueryInterface() FAILED, riid=%s", this, + GetRIIDNameStr(riid).get())); + return E_NOINTERFACE; +} + +STDMETHODIMP +TSFTextStore::AdviseSink(REFIID riid, IUnknown* punk, DWORD dwMask) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseSink(riid=%s, punk=0x%p, dwMask=%s), " + "mSink=0x%p, mSinkMask=%s", + this, GetRIIDNameStr(riid).get(), punk, GetSinkMaskNameStr(dwMask).get(), + mSink.get(), GetSinkMaskNameStr(mSinkMask).get())); + + if (!punk) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to the null punk", + this)); + return E_UNEXPECTED; + } + + if (IID_ITextStoreACPSink != riid) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to " + "unsupported interface", + this)); + return E_INVALIDARG; // means unsupported interface. + } + + if (!mSink) { + // Install sink + punk->QueryInterface(IID_ITextStoreACPSink, getter_AddRefs(mSink)); + if (!mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to " + "punk not having the interface", + this)); + return E_UNEXPECTED; + } + } else { + // If sink is already installed we check to see if they are the same + // Get IUnknown from both sides for comparison + RefPtr<IUnknown> comparison1, comparison2; + punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1)); + mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2)); + if (comparison1 != comparison2) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to " + "the sink being different from the stored sink", + this)); + return CONNECT_E_ADVISELIMIT; + } + } + // Update mask either for a new sink or an existing sink + mSinkMask = dwMask; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::UnadviseSink(IUnknown* punk) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseSink(punk=0x%p), mSink=0x%p", this, punk, + mSink.get())); + + if (!punk) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseSink() FAILED due to the null punk", + this)); + return E_INVALIDARG; + } + if (!mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseSink() FAILED due to " + "any sink not stored", + this)); + return CONNECT_E_NOCONNECTION; + } + // Get IUnknown from both sides for comparison + RefPtr<IUnknown> comparison1, comparison2; + punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1)); + mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2)); + // Unadvise only if sinks are the same + if (comparison1 != comparison2) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseSink() FAILED due to " + "the sink being different from the stored sink", + this)); + return CONNECT_E_NOCONNECTION; + } + mSink = nullptr; + mSinkMask = 0; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::RequestLock(DWORD dwLockFlags, HRESULT* phrSession) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock(dwLockFlags=%s, phrSession=0x%p), " + "mLock=%s, mDestroyed=%s", + this, GetLockFlagNameStr(dwLockFlags).get(), phrSession, + GetLockFlagNameStr(mLock).get(), GetBoolName(mDestroyed))); + + if (!mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RequestLock() FAILED due to " + "any sink not stored", + this)); + return E_FAIL; + } + if (mDestroyed && + (mContentForTSF.isNothing() || mSelectionForTSF.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RequestLock() FAILED due to " + "being destroyed and no information of the contents", + this)); + return E_FAIL; + } + if (!phrSession) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RequestLock() FAILED due to " + "null phrSession", + this)); + return E_INVALIDARG; + } + + if (!mLock) { + // put on lock + mLock = dwLockFlags & (~TS_LF_SYNC); + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p Locking (%s) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", + this, GetLockFlagNameStr(mLock).get())); + // Don't release this instance during this lock because this is called by + // TSF but they don't grab us during this call. + RefPtr<TSFTextStore> kungFuDeathGrip(this); + RefPtr<ITextStoreACPSink> sink = mSink; + *phrSession = sink->OnLockGranted(mLock); + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", + this, GetLockFlagNameStr(mLock).get())); + DidLockGranted(); + while (mLockQueued) { + mLock = mLockQueued; + mLockQueued = 0; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p Locking for the request in the queue (%s) >>>>>>>>>>>>>>" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + ">>>>>", + this, GetLockFlagNameStr(mLock).get())); + sink->OnLockGranted(mLock); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<", + this, GetLockFlagNameStr(mLock).get())); + DidLockGranted(); + } + + // The document is now completely unlocked. + mLock = 0; + + MaybeFlushPendingNotifications(); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock() succeeded: *phrSession=%s", + this, GetTextStoreReturnValueName(*phrSession))); + return S_OK; + } + + // only time when reentrant lock is allowed is when caller holds a + // read-only lock and is requesting an async write lock + if (IsReadLocked() && !IsReadWriteLocked() && IsReadWriteLock(dwLockFlags) && + !(dwLockFlags & TS_LF_SYNC)) { + *phrSession = TS_S_ASYNC; + mLockQueued = dwLockFlags & (~TS_LF_SYNC); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock() stores the request in the " + "queue, *phrSession=TS_S_ASYNC", + this)); + return S_OK; + } + + // no more locks allowed + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock() didn't allow to lock, " + "*phrSession=TS_E_SYNCHRONOUS", + this)); + *phrSession = TS_E_SYNCHRONOUS; + return E_FAIL; +} + +void TSFTextStore::DidLockGranted() { + if (IsReadWriteLocked()) { + // FreeCJ (TIP for Traditional Chinese) calls SetSelection() to set caret + // to the start of composition string and insert a full width space for + // a placeholder with a call of SetText(). After that, it calls + // OnUpdateComposition() without new range. Therefore, let's record the + // composition update information here. + CompleteLastActionIfStillIncomplete(); + + FlushPendingActions(); + } + + // If the widget has gone, we don't need to notify anything. + if (mDestroyed || !mWidget || mWidget->Destroyed()) { + mPendingSelectionChangeData.reset(); + mHasReturnedNoLayoutError = false; + } +} + +void TSFTextStore::DispatchEvent(WidgetGUIEvent& aEvent) { + if (NS_WARN_IF(!mWidget) || NS_WARN_IF(mWidget->Destroyed())) { + return; + } + // If the event isn't a query content event, the event may be handled + // asynchronously. So, we should put off to answer from GetTextExt() etc. + if (!aEvent.AsQueryContentEvent()) { + mDeferNotifyingTSFUntilNextUpdate = true; + } + mWidget->DispatchWindowEvent(aEvent); +} + +void TSFTextStore::FlushPendingActions() { + if (!mWidget || mWidget->Destroyed()) { + // Note that don't clear mContentForTSF because TIP may try to commit + // composition with a document lock. In such case, TSFTextStore needs to + // behave as expected by TIP. + mPendingActions.Clear(); + mPendingSelectionChangeData.reset(); + mHasReturnedNoLayoutError = false; + return; + } + + // Some TIP may request lock but does nothing during the lock. In such case, + // this should do nothing. For example, when MS-IME for Japanese is active + // and we're inactivating, this case occurs and causes different behavior + // from the other TIPs. + if (mPendingActions.IsEmpty()) { + return; + } + + RefPtr<nsWindow> widget(mWidget); + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED due to BeginNativeInputTransaction() failure", + this)); + return; + } + for (uint32_t i = 0; i < mPendingActions.Length(); i++) { + PendingAction& action = mPendingActions[i]; + switch (action.mType) { + case PendingAction::Type::eKeyboardEvent: + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::FlushPendingActions() " + "IGNORED pending KeyboardEvent(%s) due to already destroyed", + this, + action.mKeyMsg.message == WM_KEYDOWN ? "eKeyDown" : "eKeyUp")); + } + MOZ_DIAGNOSTIC_ASSERT(action.mKeyMsg.message == WM_KEYDOWN || + action.mKeyMsg.message == WM_KEYUP); + DispatchKeyboardEventAsProcessedByIME(action.mKeyMsg); + if (!widget || widget->Destroyed()) { + break; + } + break; + case PendingAction::Type::eCompositionStart: { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eCompositionStart={ mSelectionStart=%ld, " + "mSelectionLength=%ld }, mDestroyed=%s", + this, action.mSelectionStart, action.mSelectionLength, + GetBoolName(mDestroyed))); + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::FlushPendingActions() " + "IGNORED pending compositionstart due to already destroyed", + this)); + break; + } + + if (action.mAdjustSelection) { + // Select composition range so the new composition replaces the range + WidgetSelectionEvent selectionSet(true, eSetSelection, widget); + widget->InitEvent(selectionSet); + selectionSet.mOffset = static_cast<uint32_t>(action.mSelectionStart); + selectionSet.mLength = static_cast<uint32_t>(action.mSelectionLength); + selectionSet.mReversed = false; + selectionSet.mExpandToClusterBoundary = + TSFStaticSink::ActiveTIP() != + TextInputProcessorID::eKeymanDesktop && + StaticPrefs:: + intl_tsf_hack_extend_setting_selection_range_to_cluster_boundaries(); + DispatchEvent(selectionSet); + if (!selectionSet.mSucceeded) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED due to eSetSelection failure", + this)); + break; + } + } + + // eCompositionStart always causes + // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. Therefore, we should + // wait to clear mContentForTSF until it's notified. + mDeferClearingContentForTSF = true; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "dispatching compositionstart event...", + this)); + WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->StartComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositionstart event, " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } + if (!widget || widget->Destroyed()) { + break; + } + break; + } + case PendingAction::Type::eCompositionUpdate: { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eCompositionUpdate={ mData=\"%s\", " + "mRanges=0x%p, mRanges->Length()=%zu }", + this, GetEscapedUTF8String(action.mData).get(), + action.mRanges.get(), + action.mRanges ? action.mRanges->Length() : 0)); + + // eCompositionChange causes a DOM text event, the IME will be notified + // of NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. In this case, we + // should not clear mContentForTSF until we notify the IME of the + // composition update. + mDeferClearingContentForTSF = true; + + rv = mDispatcher->SetPendingComposition(action.mData, action.mRanges); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to setting pending composition... " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } else { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "dispatching compositionchange event...", + this)); + WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->FlushPendingComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositionchange event, " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } + // Be aware, the mWidget might already have been destroyed. + } + break; + } + case PendingAction::Type::eCompositionEnd: { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eCompositionEnd={ mData=\"%s\" }", + this, GetEscapedUTF8String(action.mData).get())); + + // Dispatching eCompositionCommit causes a DOM text event, then, + // the IME will be notified of NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED + // when focused content actually handles the event. For example, + // when focused content is in a remote process, it's sent when + // all dispatched composition events have been handled in the remote + // process. So, until then, we don't have newer content information. + // Therefore, we need to put off to clear mContentForTSF. + mDeferClearingContentForTSF = true; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions(), " + "dispatching compositioncommit event...", + this)); + WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->CommitComposition(status, &action.mData, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositioncommit event, " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } + break; + } + case PendingAction::Type::eSetSelection: { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eSetSelection={ mSelectionStart=%ld, " + "mSelectionLength=%ld, mSelectionReversed=%s }, " + "mDestroyed=%s", + this, action.mSelectionStart, action.mSelectionLength, + GetBoolName(action.mSelectionReversed), GetBoolName(mDestroyed))); + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::FlushPendingActions() " + "IGNORED pending selectionset due to already destroyed", + this)); + break; + } + + WidgetSelectionEvent selectionSet(true, eSetSelection, widget); + selectionSet.mOffset = static_cast<uint32_t>(action.mSelectionStart); + selectionSet.mLength = static_cast<uint32_t>(action.mSelectionLength); + selectionSet.mReversed = action.mSelectionReversed; + selectionSet.mExpandToClusterBoundary = + TSFStaticSink::ActiveTIP() != + TextInputProcessorID::eKeymanDesktop && + StaticPrefs:: + intl_tsf_hack_extend_setting_selection_range_to_cluster_boundaries(); + DispatchEvent(selectionSet); + if (!selectionSet.mSucceeded) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED due to eSetSelection failure", + this)); + break; + } + break; + } + default: + MOZ_CRASH("unexpected action type"); + } + + if (widget && !widget->Destroyed()) { + continue; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::FlushPendingActions(), " + "qutting since the mWidget has gone", + this)); + break; + } + mPendingActions.Clear(); +} + +void TSFTextStore::MaybeFlushPendingNotifications() { + if (mDeferNotifyingTSF) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "putting off flushing pending notifications due to initializing " + "something...", + this)); + return; + } + + if (IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "putting off flushing pending notifications due to being the " + "document locked...", + this)); + return; + } + + if (mDeferCommittingComposition) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::CommitCompositionInternal(false)...", + this)); + mDeferCommittingComposition = mDeferCancellingComposition = false; + CommitCompositionInternal(false); + } else if (mDeferCancellingComposition) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::CommitCompositionInternal(true)...", + this)); + mDeferCommittingComposition = mDeferCancellingComposition = false; + CommitCompositionInternal(true); + } + + if (mDeferNotifyingTSFUntilNextUpdate) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "putting off flushing pending notifications due to being " + "dispatching events...", + this)); + return; + } + + if (mPendingDestroy) { + Destroy(); + return; + } + + if (mDestroyed) { + // If it's already been destroyed completely, this shouldn't notify TSF of + // anything anymore. + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "does nothing because this has already destroyed completely...", + this)); + return; + } + + if (!mDeferClearingContentForTSF && mContentForTSF.isSome()) { + mContentForTSF.reset(); + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "mContentForTSF is set to `Nothing`", + this)); + } + + // When there is no cached content, we can sync actual contents and TSF/TIP + // expecting contents. + RefPtr<TSFTextStore> kungFuDeathGrip = this; + Unused << kungFuDeathGrip; + if (mContentForTSF.isNothing()) { + if (mPendingTextChangeData.IsValid()) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfTextChange()...", + this)); + NotifyTSFOfTextChange(); + } + if (mPendingSelectionChangeData.isSome()) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfSelectionChange()...", + this)); + NotifyTSFOfSelectionChange(); + } + } + + if (mHasReturnedNoLayoutError) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfLayoutChange()...", + this)); + NotifyTSFOfLayoutChange(); + } +} + +void TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME() { + // If we've already been destroyed, we cannot do anything. + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "does nothing because it's already been destroyed", + this)); + return; + } + + // If we're not handling key message or we've already dispatched a keyboard + // event for the handling key message, we should do nothing anymore. + if (!sHandlingKeyMsg || sIsKeyboardEventDispatched) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "does nothing because not necessary to dispatch keyboard event", + this)); + return; + } + + sIsKeyboardEventDispatched = true; + // If the document is locked, just adding the task to dispatching an event + // to the queue. + if (IsReadLocked()) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "adding to dispatch a keyboard event into the queue...", + this)); + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eKeyboardEvent; + memcpy(&action->mKeyMsg, sHandlingKeyMsg, sizeof(MSG)); + return; + } + + // Otherwise, dispatch a keyboard event. + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "trying to dispatch a keyboard event...", + this)); + DispatchKeyboardEventAsProcessedByIME(*sHandlingKeyMsg); +} + +void TSFTextStore::DispatchKeyboardEventAsProcessedByIME(const MSG& aMsg) { + MOZ_ASSERT(mWidget); + MOZ_ASSERT(!mWidget->Destroyed()); + MOZ_ASSERT(!mDestroyed); + + ModifierKeyState modKeyState; + MSG msg(aMsg); + msg.wParam = VK_PROCESSKEY; + NativeKey nativeKey(mWidget, msg, modKeyState); + switch (aMsg.message) { + case WM_KEYDOWN: + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), " + "dispatching an eKeyDown event...", + this)); + nativeKey.HandleKeyDownMessage(); + break; + case WM_KEYUP: + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), " + "dispatching an eKeyUp event...", + this)); + nativeKey.HandleKeyUpMessage(); + break; + default: + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), " + "ERROR, it doesn't handle the message", + this)); + break; + } +} + +STDMETHODIMP +TSFTextStore::GetStatus(TS_STATUS* pdcs) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetStatus(pdcs=0x%p)", this, pdcs)); + + if (!pdcs) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetStatus() FAILED due to null pdcs", this)); + return E_INVALIDARG; + } + // We manage on-screen keyboard by own. + pdcs->dwDynamicFlags = TS_SD_INPUTPANEMANUALDISPLAYENABLE; + // we use a "flat" text model for TSF support so no hidden text + pdcs->dwStaticFlags = TS_SS_NOHIDDENTEXT; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::QueryInsert(LONG acpTestStart, LONG acpTestEnd, ULONG cch, + LONG* pacpResultStart, LONG* pacpResultEnd) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::QueryInsert(acpTestStart=%ld, " + "acpTestEnd=%ld, cch=%lu, pacpResultStart=0x%p, pacpResultEnd=0x%p)", + this, acpTestStart, acpTestEnd, cch, pacpResultStart, pacpResultEnd)); + + if (!pacpResultStart || !pacpResultEnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::QueryInsert() FAILED due to " + "the null argument", + this)); + return E_INVALIDARG; + } + + if (acpTestStart < 0 || acpTestStart > acpTestEnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::QueryInsert() FAILED due to " + "wrong argument", + this)); + return E_INVALIDARG; + } + + // XXX need to adjust to cluster boundary + // Assume we are given good offsets for now + if (mComposition.isNothing() && + ((StaticPrefs:: + intl_tsf_hack_ms_traditional_chinese_query_insert_result() && + TSFStaticSink::IsMSChangJieOrMSQuickActive()) || + (StaticPrefs:: + intl_tsf_hack_ms_simplified_chinese_query_insert_result() && + TSFStaticSink::IsMSPinyinOrMSWubiActive()))) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::QueryInsert() WARNING using different " + "result for the TIP", + this)); + // Chinese TIPs of Microsoft assume that QueryInsert() returns selected + // range which should be removed. + *pacpResultStart = acpTestStart; + *pacpResultEnd = acpTestEnd; + } else { + *pacpResultStart = acpTestStart; + *pacpResultEnd = acpTestStart + cch; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::QueryInsert() succeeded: " + "*pacpResultStart=%ld, *pacpResultEnd=%ld)", + this, *pacpResultStart, *pacpResultEnd)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetSelection(ULONG ulIndex, ULONG ulCount, + TS_SELECTION_ACP* pSelection, ULONG* pcFetched) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetSelection(ulIndex=%lu, ulCount=%lu, " + "pSelection=0x%p, pcFetched=0x%p)", + this, ulIndex, ulCount, pSelection, pcFetched)); + + if (!IsReadLocked()) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to not locked", this)); + return TS_E_NOLOCK; + } + if (!ulCount || !pSelection || !pcFetched) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + *pcFetched = 0; + + if (ulIndex != static_cast<ULONG>(TS_DEFAULT_SELECTION) && ulIndex != 0) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "unsupported selection", + this)); + return TS_E_NOSELECTION; + } + + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + if (DoNotReturnErrorFromGetSelection()) { + *pSelection = Selection::EmptyACP(); + *pcFetched = 1; + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetSelection() returns fake selection range " + "for avoiding a crash in TSF, *pSelection=%s", + this, mozilla::ToString(*pSelection).c_str())); + return S_OK; + } + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "SelectionForTSF() failure", + this)); + return E_FAIL; + } + if (!selectionForTSF->HasRange()) { + *pSelection = Selection::EmptyACP(); + *pcFetched = 0; + return TS_E_NOSELECTION; + } + *pSelection = selectionForTSF->ACPRef(); + *pcFetched = 1; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetSelection() succeeded, *pSelection=%s", + this, mozilla::ToString(*pSelection).c_str())); + return S_OK; +} + +// static +bool TSFTextStore::DoNotReturnErrorFromGetSelection() { + // There is a crash bug of TSF if we return error from GetSelection(). + // That was introduced in Anniversary Update (build 14393, see bug 1312302) + // TODO: We should avoid to run this hack on fixed builds. When we get + // exact build number, we should get back here. + static bool sTSFMayCrashIfGetSelectionReturnsError = + IsWin10AnniversaryUpdateOrLater(); + return sTSFMayCrashIfGetSelectionReturnsError; +} + +Maybe<TSFTextStore::Content>& TSFTextStore::ContentForTSF() { + // This should be called when the document is locked or the content hasn't + // been abandoned yet. + if (NS_WARN_IF(!IsReadLocked() && mContentForTSF.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to " + "called wrong timing, IsReadLocked()=%s, mContentForTSF=Nothing", + this, GetBoolName(IsReadLocked()))); + return mContentForTSF; + } + + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to " + "SelectionForTSF() failure", + this)); + mContentForTSF.reset(); + return mContentForTSF; + } + + if (mContentForTSF.isNothing()) { + MOZ_DIAGNOSTIC_ASSERT( + !mIsInitializingContentForTSF, + "TSFTextStore::ContentForTSF() shouldn't be called recursively"); + + AutoNotifyingTSFBatch deferNotifyingTSF(*this); + AutoRestore<bool> saveInitializingContetTSF(mIsInitializingContentForTSF); + mIsInitializingContentForTSF = true; + + nsString text; // Don't use auto string for avoiding to copy long string. + if (NS_WARN_IF(!GetCurrentText(text))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to " + "GetCurrentText() failure", + this)); + return mContentForTSF; + } + + MOZ_DIAGNOSTIC_ASSERT(mContentForTSF.isNothing(), + "How was it initialized recursively?"); + mContentForTSF.reset(); // For avoiding crash in release channel + mContentForTSF.emplace(*this, text); + // Basically, the cached content which is expected by TSF/TIP should be + // cleared after active composition is committed or the document lock is + // unlocked. However, in e10s mode, content will be modified + // asynchronously. In such case, mDeferClearingContentForTSF may be + // true until whole dispatched events are handled by the focused editor. + mDeferClearingContentForTSF = false; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::ContentForTSF(): mContentForTSF=%s", this, + mozilla::ToString(mContentForTSF).c_str())); + + return mContentForTSF; +} + +bool TSFTextStore::CanAccessActualContentDirectly() const { + if (mContentForTSF.isNothing() || mSelectionForTSF.isNothing()) { + return true; + } + + // If the cached content has been changed by something except composition, + // the content cache may be different from actual content. + if (mPendingTextChangeData.IsValid() && + !mPendingTextChangeData.mCausedOnlyByComposition) { + return false; + } + + // If the cached selection isn't changed, cached content and actual content + // should be same. + if (mPendingSelectionChangeData.isNothing()) { + return true; + } + + return mSelectionForTSF->EqualsExceptDirection(*mPendingSelectionChangeData); +} + +bool TSFTextStore::GetCurrentText(nsAString& aTextContent) { + if (mContentForTSF.isSome()) { + aTextContent = mContentForTSF->TextRef(); + return true; + } + + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(mWidget && !mWidget->Destroyed()); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetCurrentText(): " + "retrieving text from the content...", + this)); + + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + mWidget); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + mWidget->InitEvent(queryTextContentEvent); + DispatchEvent(queryTextContentEvent); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetCurrentText(), FAILED, due to " + "eQueryTextContent failure", + this)); + aTextContent.Truncate(); + return false; + } + + aTextContent = queryTextContentEvent.mReply->DataRef(); + return true; +} + +Maybe<TSFTextStore::Selection>& TSFTextStore::SelectionForTSF() { + if (mSelectionForTSF.isNothing()) { + MOZ_ASSERT(!mDestroyed); + // If the window has never been available, we should crash since working + // with broken values may make TIP confused. + if (!mWidget || mWidget->Destroyed()) { + MOZ_ASSERT_UNREACHABLE("There should be non-destroyed widget"); + } + + MOZ_DIAGNOSTIC_ASSERT( + !mIsInitializingSelectionForTSF, + "TSFTextStore::SelectionForTSF() shouldn't be called recursively"); + + AutoNotifyingTSFBatch deferNotifyingTSF(*this); + AutoRestore<bool> saveInitializingSelectionForTSF( + mIsInitializingSelectionForTSF); + mIsInitializingSelectionForTSF = true; + + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + mWidget); + mWidget->InitEvent(querySelectedTextEvent); + DispatchEvent(querySelectedTextEvent); + if (NS_WARN_IF(querySelectedTextEvent.Failed())) { + return mSelectionForTSF; + } + MOZ_DIAGNOSTIC_ASSERT(mSelectionForTSF.isNothing(), + "How was it initialized recursively?"); + mSelectionForTSF = Some(Selection(querySelectedTextEvent)); + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::SelectionForTSF() succeeded, " + "mSelectionForTSF=%s", + this, ToString(mSelectionForTSF).c_str())); + + return mSelectionForTSF; +} + +static HRESULT GetRangeExtent(ITfRange* aRange, LONG* aStart, LONG* aLength) { + RefPtr<ITfRangeACP> rangeACP; + aRange->QueryInterface(IID_ITfRangeACP, getter_AddRefs(rangeACP)); + NS_ENSURE_TRUE(rangeACP, E_FAIL); + return rangeACP->GetExtent(aStart, aLength); +} + +static TextRangeType GetGeckoSelectionValue(TF_DISPLAYATTRIBUTE& aDisplayAttr) { + switch (aDisplayAttr.bAttr) { + case TF_ATTR_TARGET_CONVERTED: + return TextRangeType::eSelectedClause; + case TF_ATTR_CONVERTED: + return TextRangeType::eConvertedClause; + case TF_ATTR_TARGET_NOTCONVERTED: + return TextRangeType::eSelectedRawClause; + default: + return TextRangeType::eRawClause; + } +} + +HRESULT +TSFTextStore::GetDisplayAttribute(ITfProperty* aAttrProperty, ITfRange* aRange, + TF_DISPLAYATTRIBUTE* aResult) { + NS_ENSURE_TRUE(aAttrProperty, E_FAIL); + NS_ENSURE_TRUE(aRange, E_FAIL); + NS_ENSURE_TRUE(aResult, E_FAIL); + + HRESULT hr; + + if (MOZ_LOG_TEST(gIMELog, LogLevel::Debug)) { + LONG start = 0, length = 0; + hr = GetRangeExtent(aRange, &start, &length); + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetDisplayAttribute(): " + "GetDisplayAttribute range=%ld-%ld (hr=%s)", + this, start - mComposition->StartOffset(), + start - mComposition->StartOffset() + length, + GetCommonReturnValueName(hr))); + } + + VARIANT propValue; + ::VariantInit(&propValue); + hr = aAttrProperty->GetValue(TfEditCookie(mEditCookie), aRange, &propValue); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfProperty::GetValue() failed", + this)); + return hr; + } + if (VT_I4 != propValue.vt) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfProperty::GetValue() returns non-VT_I4 value", + this)); + ::VariantClear(&propValue); + return E_FAIL; + } + + RefPtr<ITfCategoryMgr> categoryMgr = GetCategoryMgr(); + if (NS_WARN_IF(!categoryMgr)) { + return E_FAIL; + } + GUID guid; + hr = categoryMgr->GetGUID(DWORD(propValue.lVal), &guid); + ::VariantClear(&propValue); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfCategoryMgr::GetGUID() failed", + this)); + return hr; + } + + RefPtr<ITfDisplayAttributeMgr> displayAttrMgr = GetDisplayAttributeMgr(); + if (NS_WARN_IF(!displayAttrMgr)) { + return E_FAIL; + } + RefPtr<ITfDisplayAttributeInfo> info; + hr = displayAttrMgr->GetDisplayAttributeInfo(guid, getter_AddRefs(info), + nullptr); + if (FAILED(hr) || !info) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfDisplayAttributeMgr::GetDisplayAttributeInfo() failed", + this)); + return hr; + } + + hr = info->GetAttributeInfo(aResult); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfDisplayAttributeInfo::GetAttributeInfo() failed", + this)); + return hr; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetDisplayAttribute() succeeded: " + "Result={ %s }", + this, GetDisplayAttrStr(*aResult).get())); + return S_OK; +} + +HRESULT +TSFTextStore::RestartCompositionIfNecessary(ITfRange* aRangeNew) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary(" + "aRangeNew=0x%p), mComposition=%s", + this, aRangeNew, ToString(mComposition).c_str())); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED " + "due to no composition view", + this)); + return E_FAIL; + } + + HRESULT hr; + RefPtr<ITfCompositionView> pComposition(mComposition->GetView()); + RefPtr<ITfRange> composingRange(aRangeNew); + if (!composingRange) { + hr = pComposition->GetRange(getter_AddRefs(composingRange)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() " + "FAILED due to pComposition->GetRange() failure", + this)); + return hr; + } + } + + // Get starting offset of the composition + LONG compStart = 0, compLength = 0; + hr = GetRangeExtent(composingRange, &compStart, &compLength); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED " + "due to GetRangeExtent() failure", + this)); + return hr; + } + + if (mComposition->StartOffset() == compStart && + mComposition->Length() == compLength) { + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary(), " + "restaring composition because of compostion range is changed " + "(range=%ld-%ld, mComposition=%s)", + this, compStart, compStart + compLength, + ToString(mComposition).c_str())); + + // If the queried composition length is different from the length + // of our composition string, OnUpdateComposition is being called + // because a part of the original composition was committed. + hr = RestartComposition(*mComposition, pComposition, composingRange); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() " + "FAILED due to RestartComposition() failure", + this)); + return hr; + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() succeeded", this)); + return S_OK; +} + +HRESULT TSFTextStore::RestartComposition(Composition& aCurrentComposition, + ITfCompositionView* aCompositionView, + ITfRange* aNewRange) { + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + + LONG newStart, newLength; + HRESULT hr = GetRangeExtent(aNewRange, &newStart, &newLength); + LONG newEnd = newStart + newLength; + + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to SelectionForTSF() failure", + this)); + return E_FAIL; + } + + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to GetRangeExtent() failure", + this)); + return hr; + } + + // If the new range has no overlap with the crrent range, we just commit + // the composition and restart new composition with the new range but + // current selection range should be preserved. + if (newStart >= aCurrentComposition.EndOffset() || + newEnd <= aCurrentComposition.StartOffset()) { + RecordCompositionEndAction(); + RecordCompositionStartAction(aCompositionView, newStart, newLength, true); + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartComposition(aCompositionView=0x%p, " + "aNewRange=0x%p { newStart=%ld, newLength=%ld }), " + "aCurrentComposition=%s, " + "selectionForTSF=%s", + this, aCompositionView, aNewRange, newStart, newLength, + ToString(aCurrentComposition).c_str(), + ToString(selectionForTSF).c_str())); + + // If the new range has an overlap with the current one, we should not commit + // the whole current range to avoid creating an odd undo transaction. + // I.e., the overlapped range which is being composed should not appear in + // undo transaction. + + // Backup current composition data and selection data. + Composition oldComposition = aCurrentComposition; + Selection oldSelection = *selectionForTSF; + + // Commit only the part of composition. + LONG keepComposingStartOffset = + std::max(oldComposition.StartOffset(), newStart); + LONG keepComposingEndOffset = std::min(oldComposition.EndOffset(), newEnd); + MOZ_ASSERT( + keepComposingStartOffset <= keepComposingEndOffset, + "Why keepComposingEndOffset is smaller than keepComposingStartOffset?"); + LONG keepComposingLength = keepComposingEndOffset - keepComposingStartOffset; + // Remove the overlapped part from the commit string. + nsAutoString commitString(oldComposition.DataRef()); + commitString.Cut(keepComposingStartOffset - oldComposition.StartOffset(), + keepComposingLength); + // Update the composition string. + Maybe<Content>& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to ContentForTSF() failure", + this)); + return E_FAIL; + } + contentForTSF->ReplaceTextWith(oldComposition.StartOffset(), + oldComposition.Length(), commitString); + MOZ_ASSERT(mComposition.isSome()); + // Record a compositionupdate action for commit the part of composing string. + PendingAction* action = LastOrNewPendingCompositionUpdate(); + if (mComposition.isSome()) { + action->mData = mComposition->DataRef(); + } + action->mRanges->Clear(); + // Note that we shouldn't append ranges when composition string + // is empty because it may cause TextComposition confused. + if (!action->mData.IsEmpty()) { + TextRange caretRange; + caretRange.mStartOffset = caretRange.mEndOffset = static_cast<uint32_t>( + oldComposition.StartOffset() + commitString.Length()); + caretRange.mRangeType = TextRangeType::eCaret; + action->mRanges->AppendElement(caretRange); + } + action->mIncomplete = false; + + // Record compositionend action. + RecordCompositionEndAction(); + + // Record compositionstart action only with the new start since this method + // hasn't restored composing string yet. + RecordCompositionStartAction(aCompositionView, newStart, 0, false); + + // Restore the latest text content and selection. + contentForTSF->ReplaceSelectedTextWith(nsDependentSubstring( + oldComposition.DataRef(), + keepComposingStartOffset - oldComposition.StartOffset(), + keepComposingLength)); + selectionForTSF = Some(oldSelection); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartComposition() succeeded, " + "mComposition=%s, selectionForTSF=%s", + this, ToString(mComposition).c_str(), + ToString(selectionForTSF).c_str())); + + return S_OK; +} + +static bool GetColor(const TF_DA_COLOR& aTSFColor, nscolor& aResult) { + switch (aTSFColor.type) { + case TF_CT_SYSCOLOR: { + DWORD sysColor = ::GetSysColor(aTSFColor.nIndex); + aResult = + NS_RGB(GetRValue(sysColor), GetGValue(sysColor), GetBValue(sysColor)); + return true; + } + case TF_CT_COLORREF: + aResult = NS_RGB(GetRValue(aTSFColor.cr), GetGValue(aTSFColor.cr), + GetBValue(aTSFColor.cr)); + return true; + case TF_CT_NONE: + default: + return false; + } +} + +static bool GetLineStyle(TF_DA_LINESTYLE aTSFLineStyle, + TextRangeStyle::LineStyle& aTextRangeLineStyle) { + switch (aTSFLineStyle) { + case TF_LS_NONE: + aTextRangeLineStyle = TextRangeStyle::LineStyle::None; + return true; + case TF_LS_SOLID: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Solid; + return true; + case TF_LS_DOT: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Dotted; + return true; + case TF_LS_DASH: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Dashed; + return true; + case TF_LS_SQUIGGLE: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Wavy; + return true; + default: + return false; + } +} + +HRESULT +TSFTextStore::RecordCompositionUpdateAction() { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionUpdateAction(), mComposition=%s", + this, ToString(mComposition).c_str())); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to no composition view", + this)); + return E_FAIL; + } + + // Getting display attributes is *really* complicated! + // We first get the context and the property objects to query for + // attributes, but since a big range can have a variety of values for + // the attribute, we have to find out all the ranges that have distinct + // attribute values. Then we query for what the value represents through + // the display attribute manager and translate that to TextRange to be + // sent in eCompositionChange + + RefPtr<ITfProperty> attrPropetry; + HRESULT hr = + mContext->GetProperty(GUID_PROP_ATTRIBUTE, getter_AddRefs(attrPropetry)); + if (FAILED(hr) || !attrPropetry) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to mContext->GetProperty() failure", + this)); + return FAILED(hr) ? hr : E_FAIL; + } + + RefPtr<ITfRange> composingRange; + hr = mComposition->GetView()->GetRange(getter_AddRefs(composingRange)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "FAILED due to mComposition->GetView()->GetRange() failure", + this)); + return hr; + } + + RefPtr<IEnumTfRanges> enumRanges; + hr = attrPropetry->EnumRanges(TfEditCookie(mEditCookie), + getter_AddRefs(enumRanges), composingRange); + if (FAILED(hr) || !enumRanges) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to attrPropetry->EnumRanges() failure", + this)); + return FAILED(hr) ? hr : E_FAIL; + } + + // First, put the log of content and selection here. + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to SelectionForTSF() failure", + this)); + return E_FAIL; + } + + PendingAction* action = LastOrNewPendingCompositionUpdate(); + action->mData = mComposition->DataRef(); + // The ranges might already have been initialized, however, if this is + // called again, that means we need to overwrite the ranges with current + // information. + action->mRanges->Clear(); + + // Note that we shouldn't append ranges when composition string + // is empty because it may cause TextComposition confused. + if (!action->mData.IsEmpty()) { + TextRange newRange; + // No matter if we have display attribute info or not, + // we always pass in at least one range to eCompositionChange + newRange.mStartOffset = 0; + newRange.mEndOffset = action->mData.Length(); + newRange.mRangeType = TextRangeType::eRawClause; + action->mRanges->AppendElement(newRange); + + RefPtr<ITfRange> range; + while (enumRanges->Next(1, getter_AddRefs(range), nullptr) == S_OK) { + if (NS_WARN_IF(!range)) { + break; + } + + LONG rangeStart = 0, rangeLength = 0; + if (FAILED(GetRangeExtent(range, &rangeStart, &rangeLength))) { + continue; + } + // The range may include out of composition string. We should ignore + // outside of the composition string. + LONG start = std::min(std::max(rangeStart, mComposition->StartOffset()), + mComposition->EndOffset()); + LONG end = std::max( + std::min(rangeStart + rangeLength, mComposition->EndOffset()), + mComposition->StartOffset()); + LONG length = end - start; + if (length < 0) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "ignores invalid range (%ld-%ld)", + this, rangeStart - mComposition->StartOffset(), + rangeStart - mComposition->StartOffset() + rangeLength)); + continue; + } + if (!length) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "ignores a range due to outside of the composition or empty " + "(%ld-%ld)", + this, rangeStart - mComposition->StartOffset(), + rangeStart - mComposition->StartOffset() + rangeLength)); + continue; + } + + TextRange newRange; + newRange.mStartOffset = + static_cast<uint32_t>(start - mComposition->StartOffset()); + // The end of the last range in the array is + // always kept at the end of composition + newRange.mEndOffset = mComposition->Length(); + + TF_DISPLAYATTRIBUTE attr; + hr = GetDisplayAttribute(attrPropetry, range, &attr); + if (FAILED(hr)) { + newRange.mRangeType = TextRangeType::eRawClause; + } else { + newRange.mRangeType = GetGeckoSelectionValue(attr); + if (GetColor(attr.crText, newRange.mRangeStyle.mForegroundColor)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_FOREGROUND_COLOR; + } + if (GetColor(attr.crBk, newRange.mRangeStyle.mBackgroundColor)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_BACKGROUND_COLOR; + } + if (GetColor(attr.crLine, newRange.mRangeStyle.mUnderlineColor)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_UNDERLINE_COLOR; + } + if (GetLineStyle(attr.lsStyle, newRange.mRangeStyle.mLineStyle)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_LINESTYLE; + newRange.mRangeStyle.mIsBoldLine = attr.fBoldLine != 0; + } + } + + TextRange& lastRange = action->mRanges->LastElement(); + if (lastRange.mStartOffset == newRange.mStartOffset) { + // Replace range if last range is the same as this one + // So that ranges don't overlap and confuse the editor + lastRange = newRange; + } else { + lastRange.mEndOffset = newRange.mStartOffset; + action->mRanges->AppendElement(newRange); + } + } + + // We need to hack for Korean Input System which is Korean standard TIP. + // It sets no change style to IME selection (the selection is always only + // one). So, the composition string looks like normal (or committed) + // string. At this time, current selection range is same as the + // composition string range. Other applications set a wide caret which + // covers the composition string, however, Gecko doesn't support the wide + // caret drawing now (Gecko doesn't support XOR drawing), unfortunately. + // For now, we should change the range style to undefined. + if (!selectionForTSF->Collapsed() && action->mRanges->Length() == 1) { + TextRange& range = action->mRanges->ElementAt(0); + LONG start = selectionForTSF->MinOffset(); + LONG end = selectionForTSF->MaxOffset(); + if (static_cast<LONG>(range.mStartOffset) == + start - mComposition->StartOffset() && + static_cast<LONG>(range.mEndOffset) == + end - mComposition->StartOffset() && + range.mRangeStyle.IsNoChangeStyle()) { + range.mRangeStyle.Clear(); + // The looks of selected type is better than others. + range.mRangeType = TextRangeType::eSelectedRawClause; + } + } + + // The caret position has to be collapsed. + uint32_t caretPosition = static_cast<uint32_t>( + selectionForTSF->HasRange() + ? selectionForTSF->MaxOffset() - mComposition->StartOffset() + : mComposition->StartOffset()); + + // If caret is in the target clause and it doesn't have specific style, + // 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 TextRange* targetClause = action->mRanges->GetTargetClause(); + if (!targetClause || targetClause->mRangeStyle.IsDefined() || + caretPosition < targetClause->mStartOffset || + caretPosition > targetClause->mEndOffset) { + TextRange caretRange; + caretRange.mStartOffset = caretRange.mEndOffset = caretPosition; + caretRange.mRangeType = TextRangeType::eCaret; + action->mRanges->AppendElement(caretRange); + } + } + + action->mIncomplete = false; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "succeeded", + this)); + + return S_OK; +} + +HRESULT +TSFTextStore::SetSelectionInternal(const TS_SELECTION_ACP* pSelection, + bool aDispatchCompositionChangeEvent) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::SetSelectionInternal(pSelection=%s, " + "aDispatchCompositionChangeEvent=%s), mComposition=%s", + this, pSelection ? mozilla::ToString(*pSelection).c_str() : "nullptr", + GetBoolName(aDispatchCompositionChangeEvent), + ToString(mComposition).c_str())); + + MOZ_ASSERT(IsReadWriteLocked()); + + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "SelectionForTSF() failure", + this)); + return E_FAIL; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return E_FAIL; + } + + // If actually the range is not changing, we should do nothing. + // Perhaps, we can ignore the difference change because it must not be + // important for following edit. + if (selectionForTSF->EqualsExceptDirection(*pSelection)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::SetSelectionInternal() Succeeded but " + "did nothing because the selection range isn't changing", + this)); + selectionForTSF->SetSelection(*pSelection); + return S_OK; + } + + if (mComposition.isSome()) { + if (aDispatchCompositionChangeEvent) { + HRESULT hr = RestartCompositionIfNecessary(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "RestartCompositionIfNecessary() failure", + this)); + return hr; + } + } + if (pSelection->acpStart < mComposition->StartOffset() || + pSelection->acpEnd > mComposition->EndOffset()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "the selection being out of the composition string", + this)); + return TS_E_INVALIDPOS; + } + // Emulate selection during compositions + selectionForTSF->SetSelection(*pSelection); + if (aDispatchCompositionChangeEvent) { + HRESULT hr = RecordCompositionUpdateAction(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "RecordCompositionUpdateAction() failure", + this)); + return hr; + } + } + return S_OK; + } + + TS_SELECTION_ACP selectionInContent(*pSelection); + + // If mContentForTSF caches old contents which is now different from + // actual contents, we need some complicated hack here... + // Note that this hack assumes that this is used for reconversion. + if (mContentForTSF.isSome() && mPendingTextChangeData.IsValid() && + !mPendingTextChangeData.mCausedOnlyByComposition) { + uint32_t startOffset = static_cast<uint32_t>(selectionInContent.acpStart); + uint32_t endOffset = static_cast<uint32_t>(selectionInContent.acpEnd); + if (mPendingTextChangeData.mStartOffset >= endOffset) { + // Setting selection before any changed ranges is fine. + } else if (mPendingTextChangeData.mRemovedEndOffset <= startOffset) { + // Setting selection after removed range is fine with following + // adjustment. + selectionInContent.acpStart += mPendingTextChangeData.Difference(); + selectionInContent.acpEnd += mPendingTextChangeData.Difference(); + } else if (startOffset == endOffset) { + // Moving caret position may be fine in most cases even if the insertion + // point has already gone but in this case, composition will be inserted + // to unexpected position, though. + // It seems that moving caret into middle of the new text is odd. + // Perhaps, end of it is expected by users in most cases. + selectionInContent.acpStart = mPendingTextChangeData.mAddedEndOffset; + selectionInContent.acpEnd = selectionInContent.acpStart; + } else { + // Otherwise, i.e., setting range has already gone, we cannot set + // selection properly. + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "there is unknown content change", + this)); + return E_FAIL; + } + } + + CompleteLastActionIfStillIncomplete(); + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eSetSelection; + action->mSelectionStart = selectionInContent.acpStart; + action->mSelectionLength = + selectionInContent.acpEnd - selectionInContent.acpStart; + action->mSelectionReversed = (selectionInContent.style.ase == TS_AE_START); + + // Use TSF specified selection for updating mSelectionForTSF. + selectionForTSF->SetSelection(*pSelection); + + return S_OK; +} + +STDMETHODIMP +TSFTextStore::SetSelection(ULONG ulCount, const TS_SELECTION_ACP* pSelection) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetSelection(ulCount=%lu, pSelection=%s }), " + "mComposition=%s", + this, ulCount, + pSelection ? mozilla::ToString(pSelection).c_str() : "nullptr", + ToString(mComposition).c_str())); + + if (!IsReadWriteLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "not locked (read-write)", + this)); + return TS_E_NOLOCK; + } + if (ulCount != 1) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "trying setting multiple selection", + this)); + return E_INVALIDARG; + } + if (!pSelection) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + HRESULT hr = SetSelectionInternal(pSelection, true); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "SetSelectionInternal() failure", + this)); + } else { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetSelection() succeeded", this)); + } + return hr; +} + +STDMETHODIMP +TSFTextStore::GetText(LONG acpStart, LONG acpEnd, WCHAR* pchPlain, + ULONG cchPlainReq, ULONG* pcchPlainOut, + TS_RUNINFO* prgRunInfo, ULONG ulRunInfoReq, + ULONG* pulRunInfoOut, LONG* pacpNext) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetText(acpStart=%ld, acpEnd=%ld, pchPlain=0x%p, " + "cchPlainReq=%lu, pcchPlainOut=0x%p, prgRunInfo=0x%p, ulRunInfoReq=%lu, " + "pulRunInfoOut=0x%p, pacpNext=0x%p), mComposition=%s", + this, acpStart, acpEnd, pchPlain, cchPlainReq, pcchPlainOut, prgRunInfo, + ulRunInfoReq, pulRunInfoOut, pacpNext, ToString(mComposition).c_str())); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (!pcchPlainOut || (!pchPlain && !prgRunInfo) || + !cchPlainReq != !pchPlain || !ulRunInfoReq != !prgRunInfo) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "invalid argument", + this)); + return E_INVALIDARG; + } + + if (acpStart < 0 || acpEnd < -1 || (acpEnd != -1 && acpStart > acpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "invalid position", + this)); + return TS_E_INVALIDPOS; + } + + // Making sure to null-terminate string just to be on the safe side + *pcchPlainOut = 0; + if (pchPlain && cchPlainReq) *pchPlain = 0; + if (pulRunInfoOut) *pulRunInfoOut = 0; + if (pacpNext) *pacpNext = acpStart; + if (prgRunInfo && ulRunInfoReq) { + prgRunInfo->uCount = 0; + prgRunInfo->type = TS_RT_PLAIN; + } + + Maybe<Content>& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "ContentForTSF() failure", + this)); + return E_FAIL; + } + if (contentForTSF->TextRef().Length() < static_cast<uint32_t>(acpStart)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "acpStart is larger offset than the actual text length", + this)); + return TS_E_INVALIDPOS; + } + if (acpEnd != -1 && + contentForTSF->TextRef().Length() < static_cast<uint32_t>(acpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "acpEnd is larger offset than the actual text length", + this)); + return TS_E_INVALIDPOS; + } + uint32_t length = (acpEnd == -1) ? contentForTSF->TextRef().Length() - + static_cast<uint32_t>(acpStart) + : static_cast<uint32_t>(acpEnd - acpStart); + if (cchPlainReq && cchPlainReq - 1 < length) { + length = cchPlainReq - 1; + } + if (length) { + if (pchPlain && cchPlainReq) { + const char16_t* startChar = + contentForTSF->TextRef().BeginReading() + acpStart; + memcpy(pchPlain, startChar, length * sizeof(*pchPlain)); + pchPlain[length] = 0; + *pcchPlainOut = length; + } + if (prgRunInfo && ulRunInfoReq) { + prgRunInfo->uCount = length; + prgRunInfo->type = TS_RT_PLAIN; + if (pulRunInfoOut) *pulRunInfoOut = 1; + } + if (pacpNext) *pacpNext = acpStart + length; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetText() succeeded: pcchPlainOut=0x%p, " + "*prgRunInfo={ uCount=%lu, type=%s }, *pulRunInfoOut=%lu, " + "*pacpNext=%ld)", + this, pcchPlainOut, prgRunInfo ? prgRunInfo->uCount : 0, + prgRunInfo ? GetTextRunTypeName(prgRunInfo->type) : "N/A", + pulRunInfoOut ? *pulRunInfoOut : 0, pacpNext ? *pacpNext : 0)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::SetText(DWORD dwFlags, LONG acpStart, LONG acpEnd, + const WCHAR* pchText, ULONG cch, TS_TEXTCHANGE* pChange) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetText(dwFlags=%s, acpStart=%ld, acpEnd=%ld, " + "pchText=0x%p \"%s\", cch=%lu, pChange=0x%p), mComposition=%s", + this, dwFlags == TS_ST_CORRECTION ? "TS_ST_CORRECTION" : "not-specified", + acpStart, acpEnd, pchText, + pchText && cch ? GetEscapedUTF8String(pchText, cch).get() : "", cch, + pChange, ToString(mComposition).c_str())); + + // Per SDK documentation, and since we don't have better + // ways to do this, this method acts as a helper to + // call SetSelection followed by InsertTextAtSelection + if (!IsReadWriteLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetText() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + TS_SELECTION_ACP selection; + selection.acpStart = acpStart; + selection.acpEnd = acpEnd; + selection.style.ase = TS_AE_END; + selection.style.fInterimChar = 0; + // Set selection to desired range + HRESULT hr = SetSelectionInternal(&selection); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetText() FAILED due to " + "SetSelectionInternal() failure", + this)); + return hr; + } + // Replace just selected text + if (!InsertTextAtSelectionInternal(nsDependentSubstring(pchText, cch), + pChange)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetText() FAILED due to " + "InsertTextAtSelectionInternal() failure", + this)); + return E_FAIL; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetText() succeeded: pChange={ " + "acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld }", + this, pChange ? pChange->acpStart : 0, + pChange ? pChange->acpOldEnd : 0, pChange ? pChange->acpNewEnd : 0)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetFormattedText(LONG acpStart, LONG acpEnd, + IDataObject** ppDataObject) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetFormattedText() called " + "but not supported (E_NOTIMPL)", + this)); + + // no support for formatted text + return E_NOTIMPL; +} + +STDMETHODIMP +TSFTextStore::GetEmbedded(LONG acpPos, REFGUID rguidService, REFIID riid, + IUnknown** ppunk) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetEmbedded() called " + "but not supported (E_NOTIMPL)", + this)); + + // embedded objects are not supported + return E_NOTIMPL; +} + +STDMETHODIMP +TSFTextStore::QueryInsertEmbedded(const GUID* pguidService, + const FORMATETC* pFormatEtc, + BOOL* pfInsertable) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::QueryInsertEmbedded() called " + "but not supported, *pfInsertable=FALSE (S_OK)", + this)); + + // embedded objects are not supported + *pfInsertable = FALSE; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::InsertEmbedded(DWORD dwFlags, LONG acpStart, LONG acpEnd, + IDataObject* pDataObject, TS_TEXTCHANGE* pChange) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertEmbedded() called " + "but not supported (E_NOTIMPL)", + this)); + + // embedded objects are not supported + return E_NOTIMPL; +} + +// static +bool TSFTextStore::ShouldSetInputScopeOfURLBarToDefault() { + // FYI: Google Japanese Input may be an IMM-IME. If it's installed on + // Win7, it's always IMM-IME. Otherwise, basically, it's a TIP. + // However, if it's installed on Win7 and has not been updated yet + // after the OS is upgraded to Win8 or later, it's still an IMM-IME. + // Therefore, we also need to check with IMMHandler here. + if (!StaticPrefs::intl_ime_hack_set_input_scope_of_url_bar_to_default()) { + return false; + } + + if (IMMHandler::IsGoogleJapaneseInputActive()) { + return true; + } + + switch (TSFStaticSink::ActiveTIP()) { + case TextInputProcessorID::eMicrosoftIMEForJapanese: + case TextInputProcessorID::eGoogleJapaneseInput: + case TextInputProcessorID::eMicrosoftBopomofo: + case TextInputProcessorID::eMicrosoftChangJie: + case TextInputProcessorID::eMicrosoftPhonetic: + case TextInputProcessorID::eMicrosoftQuick: + case TextInputProcessorID::eMicrosoftNewChangJie: + case TextInputProcessorID::eMicrosoftNewPhonetic: + case TextInputProcessorID::eMicrosoftNewQuick: + case TextInputProcessorID::eMicrosoftPinyin: + case TextInputProcessorID::eMicrosoftPinyinNewExperienceInputStyle: + case TextInputProcessorID::eMicrosoftOldHangul: + case TextInputProcessorID::eMicrosoftWubi: + case TextInputProcessorID::eMicrosoftIMEForKorean: + return true; + default: + return false; + } +} + +void TSFTextStore::SetInputScope(const nsString& aHTMLInputType, + const nsString& aHTMLInputMode) { + mInputScopes.Clear(); + + // IME may refer only first input scope, but we will append inputmode's + // input scopes too like Chrome since IME may refer it. + IMEHandler::AppendInputScopeFromType(aHTMLInputType, mInputScopes); + IMEHandler::AppendInputScopeFromInputMode(aHTMLInputMode, mInputScopes); + + if (mInPrivateBrowsing) { + mInputScopes.AppendElement(IS_PRIVATE); + } +} + +int32_t TSFTextStore::GetRequestedAttrIndex(const TS_ATTRID& aAttrID) { + if (IsEqualGUID(aAttrID, GUID_PROP_INPUTSCOPE)) { + return eInputScope; + } + if (IsEqualGUID(aAttrID, sGUID_PROP_URL)) { + return eDocumentURL; + } + if (IsEqualGUID(aAttrID, TSATTRID_Text_VerticalWriting)) { + return eTextVerticalWriting; + } + if (IsEqualGUID(aAttrID, TSATTRID_Text_Orientation)) { + return eTextOrientation; + } + return eNotSupported; +} + +TS_ATTRID +TSFTextStore::GetAttrID(int32_t aIndex) { + switch (aIndex) { + case eInputScope: + return GUID_PROP_INPUTSCOPE; + case eDocumentURL: + return sGUID_PROP_URL; + case eTextVerticalWriting: + return TSATTRID_Text_VerticalWriting; + case eTextOrientation: + return TSATTRID_Text_Orientation; + default: + MOZ_CRASH("Invalid index? Or not implemented yet?"); + return GUID_NULL; + } +} + +HRESULT +TSFTextStore::HandleRequestAttrs(DWORD aFlags, ULONG aFilterCount, + const TS_ATTRID* aFilterAttrs) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::HandleRequestAttrs(aFlags=%s, " + "aFilterCount=%lu)", + this, GetFindFlagName(aFlags).get(), aFilterCount)); + + // This is a little weird! RequestSupportedAttrs gives us advanced notice + // of a support query via RetrieveRequestedAttrs for a specific attribute. + // RetrieveRequestedAttrs needs to return valid data for all attributes we + // support, but the text service will only want the input scope object + // returned in RetrieveRequestedAttrs if the dwFlags passed in here contains + // TS_ATTR_FIND_WANT_VALUE. + for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) { + mRequestedAttrs[i] = false; + } + mRequestedAttrValues = !!(aFlags & TS_ATTR_FIND_WANT_VALUE); + + for (uint32_t i = 0; i < aFilterCount; i++) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::HandleRequestAttrs(), " + "requested attr=%s", + this, GetGUIDNameStrWithTable(aFilterAttrs[i]).get())); + int32_t index = GetRequestedAttrIndex(aFilterAttrs[i]); + if (index != eNotSupported) { + mRequestedAttrs[index] = true; + } + } + return S_OK; +} + +STDMETHODIMP +TSFTextStore::RequestSupportedAttrs(DWORD dwFlags, ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttrs) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestSupportedAttrs(dwFlags=%s, " + "cFilterAttrs=%lu)", + this, GetFindFlagName(dwFlags).get(), cFilterAttrs)); + + return HandleRequestAttrs(dwFlags, cFilterAttrs, paFilterAttrs); +} + +STDMETHODIMP +TSFTextStore::RequestAttrsAtPosition(LONG acpPos, ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttrs, + DWORD dwFlags) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestAttrsAtPosition(acpPos=%ld, " + "cFilterAttrs=%lu, dwFlags=%s)", + this, acpPos, cFilterAttrs, GetFindFlagName(dwFlags).get())); + + return HandleRequestAttrs(dwFlags | TS_ATTR_FIND_WANT_VALUE, cFilterAttrs, + paFilterAttrs); +} + +STDMETHODIMP +TSFTextStore::RequestAttrsTransitioningAtPosition(LONG acpPos, + ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttr, + DWORD dwFlags) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestAttrsTransitioningAtPosition(" + "acpPos=%ld, cFilterAttrs=%lu, dwFlags=%s) called but not supported " + "(S_OK)", + this, acpPos, cFilterAttrs, GetFindFlagName(dwFlags).get())); + + // no per character attributes defined + return S_OK; +} + +STDMETHODIMP +TSFTextStore::FindNextAttrTransition(LONG acpStart, LONG acpHalt, + ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttrs, + DWORD dwFlags, LONG* pacpNext, + BOOL* pfFound, LONG* plFoundOffset) { + if (!pacpNext || !pfFound || !plFoundOffset) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" 0x%p TSFTextStore::FindNextAttrTransition() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::FindNextAttrTransition() called " + "but not supported (S_OK)", + this)); + + // no per character attributes defined + *pacpNext = *plFoundOffset = acpHalt; + *pfFound = FALSE; + return S_OK; +} + +// To test the document URL result, define this to out put it to the stdout +// #define DEBUG_PRINT_DOCUMENT_URL + +STDMETHODIMP +TSFTextStore::RetrieveRequestedAttrs(ULONG ulCount, TS_ATTRVAL* paAttrVals, + ULONG* pcFetched) { + if (!pcFetched || !paAttrVals) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + ULONG expectedCount = 0; + for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) { + if (mRequestedAttrs[i]) { + expectedCount++; + } + } + if (ulCount < expectedCount) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() FAILED due to " + "not enough count ulCount=%lu, expectedCount=%lu", + this, ulCount, expectedCount)); + return E_INVALIDARG; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() called " + "ulCount=%lu, mRequestedAttrValues=%s", + this, ulCount, GetBoolName(mRequestedAttrValues))); + + auto GetExposingURL = [&]() -> BSTR { + const bool allowed = + StaticPrefs::intl_tsf_expose_url_allowed() && + (!mInPrivateBrowsing || + StaticPrefs::intl_tsf_expose_url_in_private_browsing_allowed()); + if (!allowed || mDocumentURL.IsEmpty()) { + BSTR emptyString = ::SysAllocString(L""); + MOZ_ASSERT( + emptyString, + "We need to return valid BSTR pointer to notify TSF of supporting it " + "with a pointer to empty string"); + return emptyString; + } + return ::SysAllocString(mDocumentURL.get()); + }; + +#ifdef DEBUG_PRINT_DOCUMENT_URL + { + BSTR exposingURL = GetExposingURL(); + printf("TSFTextStore::RetrieveRequestedAttrs: DocumentURL=\"%s\"\n", + NS_ConvertUTF16toUTF8(static_cast<char16ptr_t>(_bstr_t(exposingURL))) + .get()); + ::SysFreeString(exposingURL); + } +#endif // #ifdef DEBUG_PRINT_DOCUMENT_URL + + int32_t count = 0; + for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) { + if (!mRequestedAttrs[i]) { + continue; + } + mRequestedAttrs[i] = false; + + TS_ATTRID attrID = GetAttrID(i); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() for %s", this, + GetGUIDNameStrWithTable(attrID).get())); + + paAttrVals[count].idAttr = attrID; + paAttrVals[count].dwOverlapId = 0; + + if (!mRequestedAttrValues) { + paAttrVals[count].varValue.vt = VT_EMPTY; + } else { + switch (i) { + case eInputScope: { + paAttrVals[count].varValue.vt = VT_UNKNOWN; + RefPtr<IUnknown> inputScope = new InputScopeImpl(mInputScopes); + paAttrVals[count].varValue.punkVal = inputScope.forget().take(); + break; + } + case eDocumentURL: { + paAttrVals[count].varValue.vt = VT_BSTR; + paAttrVals[count].varValue.bstrVal = GetExposingURL(); + break; + } + case eTextVerticalWriting: { + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + paAttrVals[count].varValue.vt = VT_BOOL; + paAttrVals[count].varValue.boolVal = + selectionForTSF.isSome() && + selectionForTSF->WritingModeRef().IsVertical() + ? VARIANT_TRUE + : VARIANT_FALSE; + break; + } + case eTextOrientation: { + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + paAttrVals[count].varValue.vt = VT_I4; + paAttrVals[count].varValue.lVal = + selectionForTSF.isSome() && + selectionForTSF->WritingModeRef().IsVertical() + ? 2700 + : 0; + break; + } + default: + MOZ_CRASH("Invalid index? Or not implemented yet?"); + break; + } + } + count++; + } + + mRequestedAttrValues = false; + + if (count) { + *pcFetched = count; + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() called " + "for unknown TS_ATTRVAL, *pcFetched=0 (S_OK)", + this)); + + paAttrVals->dwOverlapId = 0; + paAttrVals->varValue.vt = VT_EMPTY; + *pcFetched = 0; + return S_OK; +} + +#undef DEBUG_PRINT_DOCUMENT_URL + +STDMETHODIMP +TSFTextStore::GetEndACP(LONG* pacp) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetEndACP(pacp=0x%p)", this, pacp)); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (!pacp) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + Maybe<Content>& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "ContentForTSF() failure", + this)); + return E_FAIL; + } + *pacp = static_cast<LONG>(contentForTSF->TextRef().Length()); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetActiveView(TsViewCookie* pvcView) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetActiveView(pvcView=0x%p)", this, pvcView)); + + if (!pvcView) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetActiveView() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + *pvcView = TEXTSTORE_DEFAULT_VIEW; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetActiveView() succeeded: *pvcView=%ld", this, + *pvcView)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetACPFromPoint(TsViewCookie vcView, const POINT* pt, + DWORD dwFlags, LONG* pacp) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetACPFromPoint(pvcView=%ld, pt=%p (x=%ld, " + "y=%ld), dwFlags=%s, pacp=%p, mDeferNotifyingTSFUntilNextUpdate=%s, " + "mWaitingQueryLayout=%s", + this, vcView, pt, pt ? pt->x : 0, pt ? pt->y : 0, + GetACPFromPointFlagName(dwFlags).get(), pacp, + GetBoolName(mDeferNotifyingTSFUntilNextUpdate), + GetBoolName(mWaitingQueryLayout))); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!pt) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "null pt", + this)); + return E_INVALIDARG; + } + + if (!pacp) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "null pacp", + this)); + return E_INVALIDARG; + } + + mWaitingQueryLayout = false; + + if (mDestroyed || + (mContentForTSF.isSome() && mContentForTSF->IsLayoutChanged())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() returned " + "TS_E_NOLAYOUT", + this)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + LayoutDeviceIntPoint ourPt(pt->x, pt->y); + // Convert to widget relative coordinates from screen's. + ourPt -= mWidget->WidgetToScreenOffset(); + + // NOTE: Don't check if the point is in the widget since the point can be + // outside of the widget if focused editor is in a XUL <panel>. + + WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint, + mWidget); + mWidget->InitEvent(queryCharAtPointEvent, &ourPt); + + // FYI: WidgetQueryContentEvent may cause flushing pending layout and it + // may cause focus change or something. + RefPtr<TSFTextStore> kungFuDeathGrip(this); + DispatchEvent(queryCharAtPointEvent); + if (!mWidget || mWidget->Destroyed()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "mWidget was destroyed during eQueryCharacterAtPoint", + this)); + return E_FAIL; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetACPFromPoint(), queryCharAtPointEvent={ " + "mReply=%s }", + this, ToString(queryCharAtPointEvent.mReply).c_str())); + + if (NS_WARN_IF(queryCharAtPointEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "eQueryCharacterAtPoint failure", + this)); + return E_FAIL; + } + + // If dwFlags isn't set and the point isn't in any character's bounding box, + // we should return TS_E_INVALIDPOINT. + if (!(dwFlags & GXFPF_NEAREST) && queryCharAtPointEvent.DidNotFindChar()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to the " + "point contained by no bounding box", + this)); + return TS_E_INVALIDPOINT; + } + + // Although, we're not sure if mTentativeCaretOffset becomes NOT_FOUND, + // let's assume that there is no content in such case. + NS_WARNING_ASSERTION(queryCharAtPointEvent.DidNotFindTentativeCaretOffset(), + "Tentative caret offset was not found"); + + uint32_t offset; + + // If dwFlags includes GXFPF_ROUND_NEAREST, we should return tentative + // caret offset (MSDN calls it "range position"). + if (dwFlags & GXFPF_ROUND_NEAREST) { + offset = queryCharAtPointEvent.mReply->mTentativeCaretOffset.valueOr(0); + } else if (queryCharAtPointEvent.FoundChar()) { + // Otherwise, we should return character offset whose bounding box contains + // the point. + offset = queryCharAtPointEvent.mReply->StartOffset(); + } else { + // If the point isn't in any character's bounding box but we need to return + // the nearest character from the point, we should *guess* the character + // offset since there is no inexpensive API to check it strictly. + // XXX If we retrieve 2 bounding boxes, one is before the offset and + // the other is after the offset, we could resolve the offset. + // However, dispatching 2 eQueryTextRect may be expensive. + + // So, use tentative offset for now. + offset = queryCharAtPointEvent.mReply->mTentativeCaretOffset.valueOr(0); + + // However, if it's after the last character, we need to decrement the + // offset. + Maybe<Content>& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "ContentForTSF() failure", + this)); + return E_FAIL; + } + if (contentForTSF->TextRef().Length() <= offset) { + // If the tentative caret is after the last character, let's return + // the last character's offset. + offset = contentForTSF->TextRef().Length() - 1; + } + } + + if (NS_WARN_IF(offset > LONG_MAX)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to out of " + "range of the result", + this)); + return TS_E_INVALIDPOINT; + } + + *pacp = static_cast<LONG>(offset); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetACPFromPoint() succeeded: *pacp=%ld", this, + *pacp)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetTextExt(TsViewCookie vcView, LONG acpStart, LONG acpEnd, + RECT* prc, BOOL* pfClipped) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetTextExt(vcView=%ld, " + "acpStart=%ld, acpEnd=%ld, prc=0x%p, pfClipped=0x%p), " + "IsHandlingCompositionInParent()=%s, " + "IsHandlingCompositionInContent()=%s, mContentForTSF=%s, " + "mSelectionForTSF=%s, mComposition=%s, " + "mDeferNotifyingTSFUntilNextUpdate=%s, mWaitingQueryLayout=%s, " + "IMEHandler::IsA11yHandlingNativeCaret()=%s", + this, vcView, acpStart, acpEnd, prc, pfClipped, + GetBoolName(IsHandlingCompositionInParent()), + GetBoolName(IsHandlingCompositionInContent()), + mozilla::ToString(mContentForTSF).c_str(), + ToString(mSelectionForTSF).c_str(), ToString(mComposition).c_str(), + GetBoolName(mDeferNotifyingTSFUntilNextUpdate), + GetBoolName(mWaitingQueryLayout), + GetBoolName(IMEHandler::IsA11yHandlingNativeCaret()))); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!prc || !pfClipped) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + // According to MSDN, ITextStoreACP::GetTextExt() should return + // TS_E_INVALIDARG when acpStart and acpEnd are same (i.e., collapsed range). + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms538435(v=vs.85).aspx + // > TS_E_INVALIDARG: The specified start and end character positions are + // > equal. + // However, some TIPs (including Microsoft's Chinese TIPs!) call this with + // collapsed range and if we return TS_E_INVALIDARG, they stops showing their + // owning window or shows it but odd position. So, we should just return + // error only when acpStart and/or acpEnd are really odd. + + if (acpStart < 0 || acpEnd < acpStart) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "invalid position", + this)); + return TS_E_INVALIDPOS; + } + + mWaitingQueryLayout = false; + + if (IsHandlingCompositionInContent() && mContentForTSF.isSome() && + mContentForTSF->HasOrHadComposition() && + mContentForTSF->IsLayoutChanged() && + mContentForTSF->MinModifiedOffset().value() > + static_cast<uint32_t>(LONG_MAX)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt(), FAILED due to the text " + "is too big for TSF (cannot treat modified offset as LONG), " + "mContentForTSF=%s", + this, ToString(mContentForTSF).c_str())); + return E_FAIL; + } + + // At Windows 10 build 17643 (an insider preview for RS5), Microsoft fixed + // the bug of TS_E_NOLAYOUT (even when we returned TS_E_NOLAYOUT, TSF + // returned E_FAIL to TIP). However, until we drop to support older Windows + // and all TIPs are aware of TS_E_NOLAYOUT result, we need to keep returning + // S_OK and available rectangle only for them. + if (!MaybeHackNoErrorLayoutBugs(acpStart, acpEnd) && + mContentForTSF.isSome() && mContentForTSF->IsLayoutChangedAt(acpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT " + "(acpEnd=%ld)", + this, acpEnd)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT " + "(acpEnd=%ld) because this has already been destroyed", + this, acpEnd)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + // use eQueryTextRect to get rect in system, screen coordinates + WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, mWidget); + mWidget->InitEvent(queryTextRectEvent); + + WidgetQueryContentEvent::Options options; + int64_t startOffset = acpStart; + if (mComposition.isSome()) { + // If there is a composition, TSF must want character rects related to + // the composition. Therefore, we should use insertion point relative + // query because the composition might be at different position from + // the position where TSFTextStore believes it at. + options.mRelativeToInsertionPoint = true; + startOffset -= mComposition->StartOffset(); + } else if (IsHandlingCompositionInParent() && mContentForTSF.isSome() && + mContentForTSF->HasOrHadComposition()) { + // If there was a composition and its commit event hasn't been dispatched + // yet, ContentCacheInParent is still open for relative offset query from + // the latest composition. + options.mRelativeToInsertionPoint = true; + startOffset -= mContentForTSF->LatestCompositionRange()->StartOffset(); + } else if (!CanAccessActualContentDirectly() && + mSelectionForTSF->HasRange()) { + // If TSF/TIP cannot access actual content directly, there may be pending + // text and/or selection changes which have not been notified TSF yet. + // Therefore, we should use relative to insertion point query since + // TSF/TIP computes the offset from the cached selection. + options.mRelativeToInsertionPoint = true; + startOffset -= mSelectionForTSF->StartOffset(); + } + // ContentEventHandler and ContentCache return actual caret rect when + // the queried range is collapsed and selection is collapsed at the + // queried range. Then, its height (in horizontal layout, width in vertical + // layout) may be different from actual font height of the line. In such + // case, users see "dancing" of candidate or suggest window of TIP. + // For preventing it, we should query text rect with at least 1 length. + uint32_t length = std::max(static_cast<int32_t>(acpEnd - acpStart), 1); + queryTextRectEvent.InitForQueryTextRect(startOffset, length, options); + + DispatchEvent(queryTextRectEvent); + if (NS_WARN_IF(queryTextRectEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "eQueryTextRect failure", + this)); + return TS_E_INVALIDPOS; // but unexpected failure, maybe. + } + + // IMEs don't like empty rects, fix here + if (queryTextRectEvent.mReply->mRect.Width() <= 0) { + queryTextRectEvent.mReply->mRect.SetWidth(1); + } + if (queryTextRectEvent.mReply->mRect.Height() <= 0) { + queryTextRectEvent.mReply->mRect.SetHeight(1); + } + + // convert to unclipped screen rect + nsWindow* refWindow = + static_cast<nsWindow*>(!!queryTextRectEvent.mReply->mFocusedWidget + ? queryTextRectEvent.mReply->mFocusedWidget + : static_cast<nsIWidget*>(mWidget.get())); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(false); + if (!refWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "no top level window", + this)); + return E_FAIL; + } + + queryTextRectEvent.mReply->mRect.MoveBy(refWindow->WidgetToScreenOffset()); + + // get bounding screen rect to test for clipping + if (!GetScreenExtInternal(*prc)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "GetScreenExtInternal() failure", + this)); + return E_FAIL; + } + + // clip text rect to bounding rect + RECT textRect; + ::SetRect(&textRect, queryTextRectEvent.mReply->mRect.X(), + queryTextRectEvent.mReply->mRect.Y(), + queryTextRectEvent.mReply->mRect.XMost(), + queryTextRectEvent.mReply->mRect.YMost()); + if (!::IntersectRect(prc, prc, &textRect)) + // Text is not visible + ::SetRectEmpty(prc); + + // not equal if text rect was clipped + *pfClipped = !::EqualRect(prc, &textRect); + + // ATOK 2011 - 2016 refers native caret position and size on windows whose + // class name is one of Mozilla's windows for deciding candidate window + // position. Additionally, ATOK 2015 and earlier behaves really odd when + // we don't create native caret. Therefore, we need to create native caret + // only when ATOK 2011 - 2015 is active (i.e., not necessary for ATOK 2016). + // However, if a11y module is handling native caret, we shouldn't touch it. + // Note that ATOK must require the latest information of the caret. So, + // even if we'll create native caret later, we need to creat it here with + // current information. + if (!IMEHandler::IsA11yHandlingNativeCaret() && + StaticPrefs::intl_tsf_hack_atok_create_native_caret() && + TSFStaticSink::IsATOKReferringNativeCaretActive() && + mComposition.isSome() && + mComposition->IsOffsetInRangeOrEndOffset(acpStart) && + mComposition->IsOffsetInRangeOrEndOffset(acpEnd)) { + CreateNativeCaret(); + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetTextExt() succeeded: " + "*prc={ left=%ld, top=%ld, right=%ld, bottom=%ld }, *pfClipped=%s", + this, prc->left, prc->top, prc->right, prc->bottom, + GetBoolName(*pfClipped))); + + return S_OK; +} + +bool TSFTextStore::MaybeHackNoErrorLayoutBugs(LONG& aACPStart, LONG& aACPEnd) { + // When ITextStoreACP::GetTextExt() returns TS_E_NOLAYOUT, TSF returns E_FAIL + // to its caller (typically, active TIP). Then, most TIPs abort current job + // or treat such application as non-GUI apps. E.g., some of them give up + // showing candidate window, some others show candidate window at top-left of + // the screen. For avoiding this issue, when there is composition (until + // composition is actually committed in remote content), we should not + // return TS_E_NOLAYOUT error for TIPs whose some features are broken by + // this issue. + // Note that ideally, this issue should be avoided by each TIP since this + // won't be fixed at least on non-latest Windows. Actually, Google Japanese + // Input (based on Mozc) does it. When GetTextExt() returns E_FAIL, TIPs + // should try to check result of GetRangeFromPoint() because TSF returns + // TS_E_NOLAYOUT correctly in this case. See: + // https://github.com/google/mozc/blob/6b878e31fb6ac4347dc9dfd8ccc1080fe718479f/src/win32/tip/tip_range_util.cc#L237-L257 + + if (!IsHandlingCompositionInContent() || mContentForTSF.isNothing() || + !mContentForTSF->HasOrHadComposition() || + !mContentForTSF->IsLayoutChangedAt(aACPEnd)) { + return false; + } + + MOZ_ASSERT(mComposition.isNothing() || + mComposition->StartOffset() == + mContentForTSF->LatestCompositionRange()->StartOffset()); + MOZ_ASSERT(mComposition.isNothing() || + mComposition->EndOffset() == + mContentForTSF->LatestCompositionRange()->EndOffset()); + + // If TSF does not have the bug, we need to hack only with a few TIPs. + static const bool sAlllowToStopHackingIfFine = + IsWindows10BuildOrLater(17643) && + StaticPrefs:: + intl_tsf_hack_allow_to_stop_hacking_on_build_17643_or_later(); + + // We need to compute active TIP now. This may take a couple of milliseconds, + // however, it'll be cached, so, must be faster than check active TIP every + // GetTextExt() calls. + const Maybe<Selection>& selectionForTSF = SelectionForTSF(); + switch (TSFStaticSink::ActiveTIP()) { + // MS IME for Japanese doesn't support asynchronous handling at deciding + // its suggest list window position. The feature was implemented + // starting from Windows 8. And also we may meet same trouble in e10s + // mode on Win7. So, we should never return TS_E_NOLAYOUT to MS IME for + // Japanese. + case TextInputProcessorID::eMicrosoftIMEForJapanese: + // Basically, MS-IME tries to retrieve whole composition string rect + // at deciding suggest window immediately after unlocking the document. + // However, in e10s mode, the content hasn't updated yet in most cases. + // Therefore, if the first character at the retrieving range rect is + // available, we should use it as the result. + // Note that according to bug 1609675, MS-IME for Japanese itself does + // not handle TS_E_NOLAYOUT correctly at least on Build 18363.657 (1909). + if (StaticPrefs:: + intl_tsf_hack_ms_japanese_ime_do_not_return_no_layout_error_at_first_char() && + aACPStart < aACPEnd) { + aACPEnd = aACPStart; + break; + } + if (sAlllowToStopHackingIfFine) { + return false; + } + // Although, the condition is not clear, MS-IME sometimes retrieves the + // caret rect immediately after modifying the composition string but + // before unlocking the document. In such case, we should return the + // nearest character rect. + // (Let's return true if there is no selection which must be not expected + // by MS-IME nor TSF.) + if (StaticPrefs:: + intl_tsf_hack_ms_japanese_ime_do_not_return_no_layout_error_at_caret() && + aACPStart == aACPEnd && selectionForTSF.isSome() && + (!selectionForTSF->HasRange() || + (selectionForTSF->Collapsed() && + selectionForTSF->EndOffset() == aACPEnd))) { + int32_t minOffsetOfLayoutChanged = + static_cast<int32_t>(mContentForTSF->MinModifiedOffset().value()); + aACPEnd = aACPStart = std::max(minOffsetOfLayoutChanged - 1, 0); + } else { + return false; + } + break; + // The bug of Microsoft Office IME 2010 for Japanese is similar to + // MS-IME for Win 8.1 and Win 10. Newer version of MS Office IME is not + // released yet. So, we can hack it without prefs because there must be + // no developers who want to disable this hack for tests. + // XXX We have not tested with Microsoft Office IME 2010 since it's + // installable only with Win7 and Win8 (i.e., cannot install Win8.1 + // and Win10), and requires upgrade to Win10. + case TextInputProcessorID::eMicrosoftOfficeIME2010ForJapanese: + // Basically, MS-IME tries to retrieve whole composition string rect + // at deciding suggest window immediately after unlocking the document. + // However, in e10s mode, the content hasn't updated yet in most cases. + // Therefore, if the first character at the retrieving range rect is + // available, we should use it as the result. + if (aACPStart < aACPEnd) { + aACPEnd = aACPStart; + } + // Although, the condition is not clear, MS-IME sometimes retrieves the + // caret rect immediately after modifying the composition string but + // before unlocking the document. In such case, we should return the + // nearest character rect. + // (Let's return true if there is no selection which must be not expected + // by MS-IME nor TSF.) + else if (aACPStart == aACPEnd && selectionForTSF.isSome() && + (!selectionForTSF->HasRange() || + (selectionForTSF->Collapsed() && + selectionForTSF->EndOffset() == aACPEnd))) { + int32_t minOffsetOfLayoutChanged = + static_cast<int32_t>(mContentForTSF->MinModifiedOffset().value()); + aACPEnd = aACPStart = std::max(minOffsetOfLayoutChanged - 1, 0); + } else { + return false; + } + break; + // ATOK fails to handle TS_E_NOLAYOUT only when it decides the position of + // suggest window. In such case, ATOK tries to query rect of whole or a + // part of composition string. + // FYI: ATOK changes their implementation around candidate window and + // suggest widget at ATOK 2016. Therefore, there are some differences + // ATOK 2015 (or older) and ATOK 2016 (or newer). + // FYI: ATOK 2017 stops referring our window class name. I.e., ATOK 2016 + // and older may behave differently only on Gecko but this must be + // finished from ATOK 2017. + // FYI: For testing with legacy ATOK, we should hack it even if current ATOK + // refers native caret rect on windows whose window class is one of + // Mozilla window classes and we stop creating native caret for ATOK + // because creating native caret causes ATOK refers caret position + // when GetTextExt() returns TS_E_NOLAYOUT. + case TextInputProcessorID::eATOK2011: + case TextInputProcessorID::eATOK2012: + case TextInputProcessorID::eATOK2013: + case TextInputProcessorID::eATOK2014: + case TextInputProcessorID::eATOK2015: + // ATOK 2016 and later may temporarily show candidate window at odd + // position when you convert a word quickly (e.g., keep pressing + // space bar). So, on ATOK 2016 or later, we need to keep hacking the + // result of GetTextExt(). + if (sAlllowToStopHackingIfFine) { + return false; + } + // If we'll create native caret where we paint our caret. Then, ATOK + // will refer native caret. So, we don't need to hack anything in + // this case. + if (StaticPrefs::intl_tsf_hack_atok_create_native_caret()) { + MOZ_ASSERT(TSFStaticSink::IsATOKReferringNativeCaretActive()); + return false; + } + [[fallthrough]]; + case TextInputProcessorID::eATOK2016: + case TextInputProcessorID::eATOKUnknown: + if (!StaticPrefs:: + intl_tsf_hack_atok_do_not_return_no_layout_error_of_composition_string()) { + return false; + } + // If the range is in the composition string, we should return rectangle + // in it as far as possible. + if (!mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPStart) || + !mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPEnd)) { + return false; + } + break; + // Japanist 10 fails to handle TS_E_NOLAYOUT when it decides the position + // of candidate window. In such case, Japanist shows candidate window at + // top-left of the screen. So, we should return the nearest caret rect + // where we know. This is Japanist's bug. So, even after build 17643, + // we need this hack. + case TextInputProcessorID::eJapanist10: + if (!StaticPrefs:: + intl_tsf_hack_japanist10_do_not_return_no_layout_error_of_composition_string()) { + return false; + } + if (!mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPStart) || + !mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPEnd)) { + return false; + } + break; + // Free ChangJie 2010 doesn't handle ITfContextView::GetTextExt() properly. + // This must be caused by the bug of TSF since Free ChangJie works fine on + // build 17643 and later. + case TextInputProcessorID::eFreeChangJie: + if (sAlllowToStopHackingIfFine) { + return false; + } + if (!StaticPrefs:: + intl_tsf_hack_free_chang_jie_do_not_return_no_layout_error()) { + return false; + } + aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset(); + aACPStart = std::min(aACPStart, aACPEnd); + break; + // Some Traditional Chinese TIPs of Microsoft don't show candidate window + // in e10s mode on Win8 or later. + case TextInputProcessorID::eMicrosoftQuick: + if (sAlllowToStopHackingIfFine) { + return false; // MS Quick works fine with Win10 build 17643. + } + [[fallthrough]]; + case TextInputProcessorID::eMicrosoftChangJie: + if (!StaticPrefs:: + intl_tsf_hack_ms_traditional_chinese_do_not_return_no_layout_error()) { + return false; + } + aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset(); + aACPStart = std::min(aACPStart, aACPEnd); + break; + // Some Simplified Chinese TIPs of Microsoft don't show candidate window + // in e10s mode on Win8 or later. + // FYI: Only Simplified Chinese TIPs of Microsoft still require this hack + // because they sometimes do not show candidate window when we return + // TS_E_NOLAYOUT for first query. Note that even when they show + // candidate window properly, we return TS_E_NOLAYOUT and following + // log looks same as when they don't show candidate window. Perhaps, + // there is stateful cause or race in them. + case TextInputProcessorID::eMicrosoftPinyin: + case TextInputProcessorID::eMicrosoftWubi: + if (!StaticPrefs:: + intl_tsf_hack_ms_simplified_chinese_do_not_return_no_layout_error()) { + return false; + } + aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset(); + aACPStart = std::min(aACPStart, aACPEnd); + break; + default: + return false; + } + + // If we hack the queried range for active TIP, that means we should not + // return TS_E_NOLAYOUT even if hacked offset is still modified. So, as + // far as possible, we should adjust the offset. + MOZ_ASSERT(mContentForTSF->IsLayoutChanged()); + bool collapsed = aACPStart == aACPEnd; + // Note that even if all characters in the editor or the composition + // string was modified, 0 or start offset of the composition string is + // useful because it may return caret rect or old character's rect which + // the user still see. That must be useful information for TIP. + int32_t firstModifiedOffset = + static_cast<int32_t>(mContentForTSF->MinModifiedOffset().value()); + LONG lastUnmodifiedOffset = std::max(firstModifiedOffset - 1, 0); + if (mContentForTSF->IsLayoutChangedAt(aACPStart)) { + if (aACPStart >= mContentForTSF->LatestCompositionRange()->StartOffset()) { + // If mContentForTSF has last composition string and current + // composition string, we can assume that ContentCacheInParent has + // cached rects of composition string at least length of current + // composition string. Otherwise, we can assume that rect for + // first character of composition string is stored since it was + // selection start or caret position. + LONG maxCachedOffset = + mContentForTSF->LatestCompositionRange()->EndOffset(); + if (mContentForTSF->LastComposition().isSome()) { + maxCachedOffset = std::min( + maxCachedOffset, mContentForTSF->LastComposition()->EndOffset()); + } + aACPStart = std::min(aACPStart, maxCachedOffset); + } + // Otherwise, we don't know which character rects are cached. So, we + // need to use first unmodified character's rect in this case. Even + // if there is no character, the query event will return caret rect + // instead. + else { + aACPStart = lastUnmodifiedOffset; + } + MOZ_ASSERT(aACPStart <= aACPEnd); + } + + // If TIP requests caret rect with collapsed range, we should keep + // collapsing the range. + if (collapsed) { + aACPEnd = aACPStart; + } + // Let's set aACPEnd to larger offset of last unmodified offset or + // aACPStart which may be the first character offset of the composition + // string. However, some TIPs may want to know the right edge of the + // range. Therefore, if aACPEnd is in composition string and active TIP + // doesn't retrieve caret rect (i.e., the range isn't collapsed), we + // should keep using the original aACPEnd. Otherwise, we should set + // aACPEnd to larger value of aACPStart and lastUnmodifiedOffset. + else if (mContentForTSF->IsLayoutChangedAt(aACPEnd) && + !mContentForTSF->LatestCompositionRange() + ->IsOffsetInRangeOrEndOffset(aACPEnd)) { + aACPEnd = std::max(aACPStart, lastUnmodifiedOffset); + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::HackNoErrorLayoutBugs() hacked the queried range " + "for not returning TS_E_NOLAYOUT, new values are: " + "aACPStart=%ld, aACPEnd=%ld", + this, aACPStart, aACPEnd)); + + return true; +} + +STDMETHODIMP +TSFTextStore::GetScreenExt(TsViewCookie vcView, RECT* prc) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetScreenExt(vcView=%ld, prc=0x%p)", this, + vcView, prc)); + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!prc) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() returns empty rect " + "due to already destroyed", + this)); + prc->left = prc->top = prc->right = prc->bottom = 0; + return S_OK; + } + + if (!GetScreenExtInternal(*prc)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "GetScreenExtInternal() failure", + this)); + return E_FAIL; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetScreenExt() succeeded: " + "*prc={ left=%ld, top=%ld, right=%ld, bottom=%ld }", + this, prc->left, prc->top, prc->right, prc->bottom)); + return S_OK; +} + +bool TSFTextStore::GetScreenExtInternal(RECT& aScreenExt) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetScreenExtInternal()", this)); + + MOZ_ASSERT(!mDestroyed); + + // use NS_QUERY_EDITOR_RECT to get rect in system, screen coordinates + WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, mWidget); + mWidget->InitEvent(queryEditorRectEvent); + DispatchEvent(queryEditorRectEvent); + if (queryEditorRectEvent.Failed()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExtInternal() FAILED due to " + "eQueryEditorRect failure", + this)); + return false; + } + + nsWindow* refWindow = + static_cast<nsWindow*>(!!queryEditorRectEvent.mReply->mFocusedWidget + ? queryEditorRectEvent.mReply->mFocusedWidget + : static_cast<nsIWidget*>(mWidget.get())); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(false); + if (!refWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExtInternal() FAILED due to " + "no top level window", + this)); + return false; + } + + LayoutDeviceIntRect boundRect = refWindow->GetClientBounds(); + boundRect.MoveTo(0, 0); + + // Clip frame rect to window rect + boundRect.IntersectRect(queryEditorRectEvent.mReply->mRect, boundRect); + if (!boundRect.IsEmpty()) { + boundRect.MoveBy(refWindow->WidgetToScreenOffset()); + ::SetRect(&aScreenExt, boundRect.X(), boundRect.Y(), boundRect.XMost(), + boundRect.YMost()); + } else { + ::SetRectEmpty(&aScreenExt); + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetScreenExtInternal() succeeded: " + "aScreenExt={ left=%ld, top=%ld, right=%ld, bottom=%ld }", + this, aScreenExt.left, aScreenExt.top, aScreenExt.right, + aScreenExt.bottom)); + return true; +} + +STDMETHODIMP +TSFTextStore::GetWnd(TsViewCookie vcView, HWND* phwnd) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetWnd(vcView=%ld, phwnd=0x%p), " + "mWidget=0x%p", + this, vcView, phwnd, mWidget.get())); + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetWnd() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!phwnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + *phwnd = mWidget ? mWidget->GetWindowHandle() : nullptr; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetWnd() succeeded: *phwnd=0x%p", this, + static_cast<void*>(*phwnd))); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::InsertTextAtSelection(DWORD dwFlags, const WCHAR* pchText, + ULONG cch, LONG* pacpStart, LONG* pacpEnd, + TS_TEXTCHANGE* pChange) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertTextAtSelection(dwFlags=%s, " + "pchText=0x%p \"%s\", cch=%lu, pacpStart=0x%p, pacpEnd=0x%p, " + "pChange=0x%p), mComposition=%s", + this, + dwFlags == 0 ? "0" + : dwFlags == TF_IAS_NOQUERY ? "TF_IAS_NOQUERY" + : dwFlags == TF_IAS_QUERYONLY ? "TF_IAS_QUERYONLY" + : "Unknown", + pchText, pchText && cch ? GetEscapedUTF8String(pchText, cch).get() : "", + cch, pacpStart, pacpEnd, pChange, ToString(mComposition).c_str())); + + if (cch && !pchText) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null pchText", + this)); + return E_INVALIDARG; + } + + if (TS_IAS_QUERYONLY == dwFlags) { + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (!pacpStart || !pacpEnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + // Get selection first + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "SelectionForTSF() failure", + this)); + return E_FAIL; + } + + // Simulate text insertion + if (selectionForTSF->HasRange()) { + *pacpStart = selectionForTSF->StartOffset(); + *pacpEnd = selectionForTSF->EndOffset(); + if (pChange) { + *pChange = TS_TEXTCHANGE{.acpStart = selectionForTSF->StartOffset(), + .acpOldEnd = selectionForTSF->EndOffset(), + .acpNewEnd = selectionForTSF->StartOffset() + + static_cast<LONG>(cch)}; + } + } else { + // There is no error code to return "no selection" state from this method. + // This means that TSF/TIP should check `GetSelection` result first and + // stop using this. However, this could be called by TIP/TSF if they do + // not do so. Therefore, we should use start of editor instead, but + // notify the caller of nothing will be inserted with pChange->acpNewEnd. + *pacpStart = *pacpEnd = 0; + if (pChange) { + *pChange = TS_TEXTCHANGE{.acpStart = 0, .acpOldEnd = 0, .acpNewEnd = 0}; + } + } + } else { + if (!IsReadWriteLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "not locked (read-write)", + this)); + return TS_E_NOLOCK; + } + + if (!pChange) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null pChange", + this)); + return E_INVALIDARG; + } + + if (TS_IAS_NOQUERY != dwFlags && (!pacpStart || !pacpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + if (!InsertTextAtSelectionInternal(nsDependentSubstring(pchText, cch), + pChange)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "InsertTextAtSelectionInternal() failure", + this)); + return E_FAIL; + } + + if (TS_IAS_NOQUERY != dwFlags) { + *pacpStart = pChange->acpStart; + *pacpEnd = pChange->acpNewEnd; + } + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertTextAtSelection() succeeded: " + "*pacpStart=%ld, *pacpEnd=%ld, " + "*pChange={ acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld })", + this, pacpStart ? *pacpStart : 0, pacpEnd ? *pacpEnd : 0, + pChange ? pChange->acpStart : 0, pChange ? pChange->acpOldEnd : 0, + pChange ? pChange->acpNewEnd : 0)); + return S_OK; +} + +bool TSFTextStore::InsertTextAtSelectionInternal(const nsAString& aInsertStr, + TS_TEXTCHANGE* aTextChange) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal(" + "aInsertStr=\"%s\", aTextChange=0x%p), mComposition=%s", + this, GetEscapedUTF8String(aInsertStr).get(), aTextChange, + ToString(mComposition).c_str())); + + Maybe<Content>& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() failed " + "due to ContentForTSF() failure()", + this)); + return false; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return false; + } + + const auto numberOfCRLFs = [&]() -> uint32_t { + const auto* str = aInsertStr.BeginReading(); + uint32_t num = 0; + for (uint32_t i = 0; i + 1 < aInsertStr.Length(); i++) { + if (str[i] == '\r' && str[i + 1] == '\n') { + num++; + i++; + } + } + return num; + }(); + if (numberOfCRLFs) { + nsAutoString key; + if (TSFStaticSink::GetActiveTIPNameForTelemetry(key)) { + Telemetry::ScalarSet( + Telemetry::ScalarID::WIDGET_IME_NAME_ON_WINDOWS_INSERTED_CRLF, key, + true); + } + } + + TS_SELECTION_ACP oldSelection = contentForTSF->Selection()->ACPRef(); + if (mComposition.isNothing()) { + // Use a temporary composition to contain the text + PendingAction* compositionStart = mPendingActions.AppendElements(2); + PendingAction* compositionEnd = compositionStart + 1; + + compositionStart->mType = PendingAction::Type::eCompositionStart; + compositionStart->mSelectionStart = oldSelection.acpStart; + compositionStart->mSelectionLength = + oldSelection.acpEnd - oldSelection.acpStart; + compositionStart->mAdjustSelection = false; + + compositionEnd->mType = PendingAction::Type::eCompositionEnd; + compositionEnd->mData = aInsertStr; + compositionEnd->mSelectionStart = compositionStart->mSelectionStart; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() " + "appending pending compositionstart and compositionend... " + "PendingCompositionStart={ mSelectionStart=%ld, " + "mSelectionLength=%ld }, PendingCompositionEnd={ mData=\"%s\" " + "(Length()=%zu), mSelectionStart=%ld }", + this, compositionStart->mSelectionStart, + compositionStart->mSelectionLength, + GetEscapedUTF8String(compositionEnd->mData).get(), + compositionEnd->mData.Length(), compositionEnd->mSelectionStart)); + } + + contentForTSF->ReplaceSelectedTextWith(aInsertStr); + + if (aTextChange) { + aTextChange->acpStart = oldSelection.acpStart; + aTextChange->acpOldEnd = oldSelection.acpEnd; + aTextChange->acpNewEnd = contentForTSF->Selection()->EndOffset(); + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() " + "succeeded: mWidget=0x%p, mWidget->Destroyed()=%s, aTextChange={ " + "acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld }", + this, mWidget.get(), GetBoolName(mWidget ? mWidget->Destroyed() : true), + aTextChange ? aTextChange->acpStart : 0, + aTextChange ? aTextChange->acpOldEnd : 0, + aTextChange ? aTextChange->acpNewEnd : 0)); + return true; +} + +STDMETHODIMP +TSFTextStore::InsertEmbeddedAtSelection(DWORD dwFlags, IDataObject* pDataObject, + LONG* pacpStart, LONG* pacpEnd, + TS_TEXTCHANGE* pChange) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertEmbeddedAtSelection() called " + "but not supported (E_NOTIMPL)", + this)); + + // embedded objects are not supported + return E_NOTIMPL; +} + +HRESULT TSFTextStore::RecordCompositionStartAction( + ITfCompositionView* aCompositionView, ITfRange* aRange, + bool aPreserveSelection) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionStartAction(" + "aCompositionView=0x%p, aRange=0x%p, aPreserveSelection=%s), " + "mComposition=%s", + this, aCompositionView, aRange, GetBoolName(aPreserveSelection), + ToString(mComposition).c_str())); + + LONG start = 0, length = 0; + HRESULT hr = GetRangeExtent(aRange, &start, &length); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to GetRangeExtent() failure", + this)); + return hr; + } + + return RecordCompositionStartAction(aCompositionView, start, length, + aPreserveSelection); +} + +HRESULT TSFTextStore::RecordCompositionStartAction( + ITfCompositionView* aCompositionView, LONG aStart, LONG aLength, + bool aPreserveSelection) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionStartAction(" + "aCompositionView=0x%p, aStart=%ld, aLength=%ld, " + "aPreserveSelection=%s), " + "mComposition=%s", + this, aCompositionView, aStart, aLength, + GetBoolName(aPreserveSelection), ToString(mComposition).c_str())); + + Maybe<Content>& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to ContentForTSF() failure", + this)); + return E_FAIL; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return false; + } + + CompleteLastActionIfStillIncomplete(); + + // TIP may have inserted text at selection before calling + // OnStartComposition(). In this case, we've already created a pending + // compositionend. If new composition replaces all commit string of the + // pending compositionend, we should cancel the pending compositionend and + // keep the previous composition normally. + // On Windows 7, MS-IME for Korean, MS-IME 2010 for Korean and MS Old Hangul + // may start composition with calling InsertTextAtSelection() and + // OnStartComposition() with this order (bug 1208043). + // On Windows 10, MS Pinyin, MS Wubi, MS ChangJie and MS Quick commits + // last character and replace it with empty string with new composition + // when user removes last character of composition string with Backspace + // key (bug 1462257). + if (!aPreserveSelection && + IsLastPendingActionCompositionEndAt(aStart, aLength)) { + const PendingAction& pendingCompositionEnd = mPendingActions.LastElement(); + contentForTSF->RestoreCommittedComposition(aCompositionView, + pendingCompositionEnd); + mPendingActions.RemoveLastElement(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionStartAction() " + "succeeded: restoring the committed string as composing string, " + "mComposition=%s, mSelectionForTSF=%s", + this, ToString(mComposition).c_str(), + ToString(mSelectionForTSF).c_str())); + return S_OK; + } + + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eCompositionStart; + action->mSelectionStart = aStart; + action->mSelectionLength = aLength; + + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to SelectionForTSF() failure", + this)); + action->mAdjustSelection = true; + } else if (!selectionForTSF->HasRange()) { + // If there is no selection, let's collapse seletion to the insertion point. + action->mAdjustSelection = true; + } else if (selectionForTSF->MinOffset() != aStart || + selectionForTSF->MaxOffset() != aStart + aLength) { + // If new composition range is different from current selection range, + // we need to set selection before dispatching compositionstart event. + action->mAdjustSelection = true; + } else { + // We shouldn't dispatch selection set event before dispatching + // compositionstart event because it may cause put caret different + // position in HTML editor since generated flat text content and offset in + // it are lossy data of HTML contents. + action->mAdjustSelection = false; + } + + contentForTSF->StartComposition(aCompositionView, *action, + aPreserveSelection); + MOZ_ASSERT(mComposition.isSome()); + action->mData = mComposition->DataRef(); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionStartAction() succeeded: " + "mComposition=%s, mSelectionForTSF=%s }", + this, ToString(mComposition).c_str(), + ToString(mSelectionForTSF).c_str())); + return S_OK; +} + +HRESULT +TSFTextStore::RecordCompositionEndAction() { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionEndAction(), " + "mComposition=%s", + this, ToString(mComposition).c_str())); + + MOZ_ASSERT(mComposition.isSome()); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due to " + "no composition", + this)); + return false; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return false; + } + + // If we're handling incomplete composition update or already handled + // composition update, we can forget them since composition end will send + // the latest composition string and it overwrites the composition string + // even if we dispatch eCompositionChange event before that. So, let's + // forget all composition updates now. + RemoveLastCompositionUpdateActions(); + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eCompositionEnd; + action->mData = mComposition->DataRef(); + action->mSelectionStart = mComposition->StartOffset(); + + Maybe<Content>& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due " + "to ContentForTSF() failure", + this)); + return E_FAIL; + } + contentForTSF->EndComposition(*action); + + // If this composition was restart but the composition doesn't modify + // anything, we should remove the pending composition for preventing to + // dispatch redundant composition events. + for (size_t i = mPendingActions.Length(), j = 1; i > 0; --i, ++j) { + PendingAction& pendingAction = mPendingActions[i - 1]; + if (pendingAction.mType == PendingAction::Type::eCompositionStart) { + if (pendingAction.mData != action->mData) { + break; + } + // When only setting selection is necessary, we should append it. + if (pendingAction.mAdjustSelection) { + LONG selectionStart = pendingAction.mSelectionStart; + LONG selectionLength = pendingAction.mSelectionLength; + + PendingAction* setSelection = mPendingActions.AppendElement(); + setSelection->mType = PendingAction::Type::eSetSelection; + setSelection->mSelectionStart = selectionStart; + setSelection->mSelectionLength = selectionLength; + setSelection->mSelectionReversed = false; + } + // Remove the redundant pending composition. + mPendingActions.RemoveElementsAt(i - 1, j); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionEndAction(), " + "succeeded, but the composition was canceled due to redundant", + this)); + return S_OK; + } + } + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionEndAction(), succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnStartComposition(ITfCompositionView* pComposition, BOOL* pfOk) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnStartComposition(pComposition=0x%p, " + "pfOk=0x%p), mComposition=%s", + this, pComposition, pfOk, ToString(mComposition).c_str())); + + AutoPendingActionAndContentFlusher flusher(this); + + *pfOk = FALSE; + + // Only one composition at a time + if (mComposition.isSome()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnStartComposition() FAILED due to " + "there is another composition already (but returns S_OK)", + this)); + return S_OK; + } + + RefPtr<ITfRange> range; + HRESULT hr = pComposition->GetRange(getter_AddRefs(range)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnStartComposition() FAILED due to " + "pComposition->GetRange() failure", + this)); + return hr; + } + hr = RecordCompositionStartAction(pComposition, range, false); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnStartComposition() FAILED due to " + "RecordCompositionStartAction() failure", + this)); + return hr; + } + + *pfOk = TRUE; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnStartComposition() succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnUpdateComposition(ITfCompositionView* pComposition, + ITfRange* pRangeNew) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition(pComposition=0x%p, " + "pRangeNew=0x%p), mComposition=%s", + this, pComposition, pRangeNew, ToString(mComposition).c_str())); + + AutoPendingActionAndContentFlusher flusher(this); + + if (!mDocumentMgr || !mContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "not ready for the composition", + this)); + return E_UNEXPECTED; + } + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "no active composition", + this)); + return E_UNEXPECTED; + } + if (mComposition->GetView() != pComposition) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "different composition view specified", + this)); + return E_UNEXPECTED; + } + + // pRangeNew is null when the update is not complete + if (!pRangeNew) { + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return E_FAIL; + } + PendingAction* action = LastOrNewPendingCompositionUpdate(); + action->mIncomplete = true; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition() succeeded but " + "not complete", + this)); + return S_OK; + } + + HRESULT hr = RestartCompositionIfNecessary(pRangeNew); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "RestartCompositionIfNecessary() failure", + this)); + return hr; + } + + hr = RecordCompositionUpdateAction(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "RecordCompositionUpdateAction() failure", + this)); + return hr; + } + + if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) { + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "SelectionForTSF() failure", + this)); + return S_OK; // Don't return error only when we're logging. + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition() succeeded: " + "mComposition=%s, SelectionForTSF()=%s", + this, ToString(mComposition).c_str(), + ToString(selectionForTSF).c_str())); + } + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnEndComposition(ITfCompositionView* pComposition) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnEndComposition(pComposition=0x%p), " + "mComposition=%s", + this, pComposition, ToString(mComposition).c_str())); + + AutoPendingActionAndContentFlusher flusher(this); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "no active composition", + this)); + return E_UNEXPECTED; + } + + if (mComposition->GetView() != pComposition) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "different composition view specified", + this)); + return E_UNEXPECTED; + } + + HRESULT hr = RecordCompositionEndAction(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "RecordCompositionEndAction() failure", + this)); + return hr; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnEndComposition(), succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::AdviseMouseSink(ITfRangeACP* range, ITfMouseSink* pSink, + DWORD* pdwCookie) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseMouseSink(range=0x%p, pSink=0x%p, " + "pdwCookie=0x%p)", + this, range, pSink, pdwCookie)); + + if (!pdwCookie) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the " + "pdwCookie is null", + this)); + return E_INVALIDARG; + } + // Initialize the result with invalid cookie for safety. + *pdwCookie = MouseTracker::kInvalidCookie; + + if (!range) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the " + "range is null", + this)); + return E_INVALIDARG; + } + if (!pSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the " + "pSink is null", + this)); + return E_INVALIDARG; + } + + // Looking for an unusing tracker. + MouseTracker* tracker = nullptr; + for (size_t i = 0; i < mMouseTrackers.Length(); i++) { + if (mMouseTrackers[i].IsUsing()) { + continue; + } + tracker = &mMouseTrackers[i]; + } + // If there is no unusing tracker, create new one. + // XXX Should we make limitation of the number of installs? + if (!tracker) { + tracker = mMouseTrackers.AppendElement(); + HRESULT hr = tracker->Init(this); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to " + "failure of MouseTracker::Init()", + this)); + return hr; + } + } + HRESULT hr = tracker->AdviseSink(this, range, pSink); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to failure " + "of MouseTracker::Init()", + this)); + return hr; + } + *pdwCookie = tracker->Cookie(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseMouseSink(), succeeded, " + "*pdwCookie=%ld", + this, *pdwCookie)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::UnadviseMouseSink(DWORD dwCookie) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseMouseSink(dwCookie=%ld)", this, dwCookie)); + if (dwCookie == MouseTracker::kInvalidCookie) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to " + "the cookie is invalid value", + this)); + return E_INVALIDARG; + } + // The cookie value must be an index of mMouseTrackers. + // We can use this shortcut for now. + if (static_cast<size_t>(dwCookie) >= mMouseTrackers.Length()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to " + "the cookie is too large value", + this)); + return E_INVALIDARG; + } + MouseTracker& tracker = mMouseTrackers[dwCookie]; + if (!tracker.IsUsing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to " + "the found tracker uninstalled already", + this)); + return E_INVALIDARG; + } + tracker.UnadviseSink(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseMouseSink(), succeeded", this)); + return S_OK; +} + +// static +nsresult TSFTextStore::OnFocusChange(bool aGotFocus, nsWindow* aFocusedWidget, + const InputContext& aContext) { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::OnFocusChange(aGotFocus=%s, " + "aFocusedWidget=0x%p, aContext=%s), " + "sThreadMgr=0x%p, sEnabledTextStore=0x%p", + GetBoolName(aGotFocus), aFocusedWidget, + mozilla::ToString(aContext).c_str(), sThreadMgr.get(), + sEnabledTextStore.get())); + + if (NS_WARN_IF(!IsInTSFMode())) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<ITfDocumentMgr> prevFocusedDocumentMgr; + bool hasFocus = ThinksHavingFocus(); + RefPtr<TSFTextStore> oldTextStore = sEnabledTextStore.forget(); + + // If currently oldTextStore still has focus, notifies TSF of losing focus. + if (hasFocus) { + RefPtr<ITfThreadMgr> threadMgr = sThreadMgr; + DebugOnly<HRESULT> hr = threadMgr->AssociateFocus( + oldTextStore->mWidget->GetWindowHandle(), nullptr, + getter_AddRefs(prevFocusedDocumentMgr)); + NS_ASSERTION(SUCCEEDED(hr), "Disassociating focus failed"); + NS_ASSERTION(prevFocusedDocumentMgr == oldTextStore->mDocumentMgr, + "different documentMgr has been associated with the window"); + } + + // Even if there was a focused TextStore, we won't use it with new focused + // editor. So, release it now. + if (oldTextStore) { + oldTextStore->Destroy(); + } + + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED, due to " + "sThreadMgr being destroyed during calling " + "ITfThreadMgr::AssociateFocus()")); + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(sEnabledTextStore)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED, due to " + "nested event handling has created another focused TextStore during " + "calling ITfThreadMgr::AssociateFocus()")); + return NS_ERROR_FAILURE; + } + + // If this is a notification of blur, move focus to the dummy document + // manager. + if (!aGotFocus || !aContext.mIMEState.IsEditable()) { + RefPtr<ITfThreadMgr> threadMgr = sThreadMgr; + RefPtr<ITfDocumentMgr> disabledDocumentMgr = sDisabledDocumentMgr; + HRESULT hr = threadMgr->SetFocus(disabledDocumentMgr); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED due to " + "ITfThreadMgr::SetFocus() failure")); + return NS_ERROR_FAILURE; + } + return NS_OK; + } + + // If an editor is getting focus, create new TextStore and set focus. + if (NS_WARN_IF(!CreateAndSetFocus(aFocusedWidget, aContext))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED due to " + "ITfThreadMgr::CreateAndSetFocus() failure")); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +// static +void TSFTextStore::EnsureToDestroyAndReleaseEnabledTextStoreIf( + RefPtr<TSFTextStore>& aTextStore) { + aTextStore->Destroy(); + if (sEnabledTextStore == aTextStore) { + sEnabledTextStore = nullptr; + } + aTextStore = nullptr; +} + +// static +bool TSFTextStore::CreateAndSetFocus(nsWindow* aFocusedWidget, + const InputContext& aContext) { + // TSF might do something which causes that we need to access static methods + // of TSFTextStore. At that time, sEnabledTextStore may be necessary. + // So, we should set sEnabledTextStore directly. + RefPtr<TSFTextStore> textStore = new TSFTextStore(); + sEnabledTextStore = textStore; + if (NS_WARN_IF(!textStore->Init(aFocusedWidget, aContext))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "TSFTextStore::Init() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + RefPtr<ITfDocumentMgr> newDocMgr = textStore->mDocumentMgr; + if (NS_WARN_IF(!newDocMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "invalid TSFTextStore::mDocumentMgr")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (aContext.mIMEState.mEnabled == IMEEnabled::Password) { + MarkContextAsKeyboardDisabled(textStore->mContext); + RefPtr<ITfContext> topContext; + newDocMgr->GetTop(getter_AddRefs(topContext)); + if (topContext && topContext != textStore->mContext) { + MarkContextAsKeyboardDisabled(topContext); + } + } + + HRESULT hr; + RefPtr<ITfThreadMgr> threadMgr = sThreadMgr; + hr = threadMgr->SetFocus(newDocMgr); + + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "ITfTheadMgr::SetFocus() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "sThreadMgr being destroyed during calling " + "ITfTheadMgr::SetFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(sEnabledTextStore != textStore)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "creating TextStore has lost focus during calling " + "ITfThreadMgr::SetFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + + // Use AssociateFocus() for ensuring that any native focus event + // never steal focus from our documentMgr. + RefPtr<ITfDocumentMgr> prevFocusedDocumentMgr; + hr = threadMgr->AssociateFocus(aFocusedWidget->GetWindowHandle(), newDocMgr, + getter_AddRefs(prevFocusedDocumentMgr)); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "ITfTheadMgr::AssociateFocus() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "sThreadMgr being destroyed during calling " + "ITfTheadMgr::AssociateFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(sEnabledTextStore != textStore)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "creating TextStore has lost focus during calling " + "ITfTheadMgr::AssociateFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + + if (textStore->mSink) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" TSFTextStore::CreateAndSetFocus(), calling " + "ITextStoreACPSink::OnLayoutChange(TS_LC_CREATE) for 0x%p...", + textStore.get())); + RefPtr<ITextStoreACPSink> sink = textStore->mSink; + sink->OnLayoutChange(TS_LC_CREATE, TEXTSTORE_DEFAULT_VIEW); + if (NS_WARN_IF(sEnabledTextStore != textStore)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "creating TextStore has lost focus during calling " + "ITextStoreACPSink::OnLayoutChange(TS_LC_CREATE)")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + } + return true; +} + +// static +IMENotificationRequests TSFTextStore::GetIMENotificationRequests() { + if (!sEnabledTextStore || NS_WARN_IF(!sEnabledTextStore->mDocumentMgr)) { + // If there is no active text store, we don't need any notifications + // since there is no sink which needs notifications. + return IMENotificationRequests(); + } + + // Otherwise, requests all notifications since even if some of them may not + // be required by the sink of active TIP, active TIP may be changed and + // other TIPs may need all notifications. + // Note that Windows temporarily steal focus from active window if the main + // process which created the window becomes busy. In this case, we shouldn't + // commit composition since user may want to continue to compose the + // composition after becoming not busy. Therefore, we need notifications + // even during deactive. + // Be aware, we don't need to check actual focused text store. For example, + // MS-IME for Japanese handles focus messages by themselves and sets focused + // text store to nullptr when the process is being inactivated. However, + // we still need to reuse sEnabledTextStore if the process is activated and + // focused element isn't changed. Therefore, if sEnabledTextStore isn't + // nullptr, we need to keep notifying the sink even when it is not focused + // text store for the thread manager. + return IMENotificationRequests( + IMENotificationRequests::NOTIFY_TEXT_CHANGE | + IMENotificationRequests::NOTIFY_POSITION_CHANGE | + IMENotificationRequests::NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR | + IMENotificationRequests::NOTIFY_DURING_DEACTIVE); +} + +nsresult TSFTextStore::OnTextChangeInternal( + const IMENotification& aIMENotification) { + const TextChangeDataBase& textChangeData = aIMENotification.mTextChangeData; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnTextChangeInternal(aIMENotification={ " + "mMessage=0x%08X, mTextChangeData=%s }), " + "mDestroyed=%s, mSink=0x%p, mSinkMask=%s, " + "mComposition=%s", + this, aIMENotification.mMessage, + mozilla::ToString(textChangeData).c_str(), GetBoolName(mDestroyed), + mSink.get(), GetSinkMaskNameStr(mSinkMask).get(), + ToString(mComposition).c_str())); + + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + mDeferNotifyingTSFUntilNextUpdate = false; + + // Different from selection change, we don't modify anything with text + // change data. Therefore, if neither TSF not TIP wants text change + // notifications, we don't need to store the changes. + if (!mSink || !(mSinkMask & TS_AS_TEXT_CHANGE)) { + return NS_OK; + } + + // Merge any text change data even if it's caused by composition. + mPendingTextChangeData.MergeWith(textChangeData); + + MaybeFlushPendingNotifications(); + + return NS_OK; +} + +void TSFTextStore::NotifyTSFOfTextChange() { + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(!IsReadLocked()); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(mPendingTextChangeData.IsValid()); + + // If the text changes are caused only by composition, we don't need to + // notify TSF of the text changes. + if (mPendingTextChangeData.mCausedOnlyByComposition) { + mPendingTextChangeData.Clear(); + return; + } + + // First, forget cached selection. + mSelectionForTSF.reset(); + + // For making it safer, we should check if there is a valid sink to receive + // text change notification. + if (NS_WARN_IF(!mSink) || NS_WARN_IF(!(mSinkMask & TS_AS_TEXT_CHANGE))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::NotifyTSFOfTextChange() FAILED due to " + "mSink is not ready to call ITextStoreACPSink::OnTextChange()...", + this)); + mPendingTextChangeData.Clear(); + return; + } + + if (NS_WARN_IF(!mPendingTextChangeData.IsInInt32Range())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::NotifyTSFOfTextChange() FAILED due to " + "offset is too big for calling " + "ITextStoreACPSink::OnTextChange()...", + this)); + mPendingTextChangeData.Clear(); + return; + } + + TS_TEXTCHANGE textChange; + textChange.acpStart = static_cast<LONG>(mPendingTextChangeData.mStartOffset); + textChange.acpOldEnd = + static_cast<LONG>(mPendingTextChangeData.mRemovedEndOffset); + textChange.acpNewEnd = + static_cast<LONG>(mPendingTextChangeData.mAddedEndOffset); + mPendingTextChangeData.Clear(); + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfTextChange(), calling " + "ITextStoreACPSink::OnTextChange(0, { acpStart=%ld, acpOldEnd=%ld, " + "acpNewEnd=%ld })...", + this, textChange.acpStart, textChange.acpOldEnd, textChange.acpNewEnd)); + RefPtr<ITextStoreACPSink> sink = mSink; + sink->OnTextChange(0, &textChange); +} + +nsresult TSFTextStore::OnSelectionChangeInternal( + const IMENotification& aIMENotification) { + const SelectionChangeDataBase& selectionChangeData = + aIMENotification.mSelectionChangeData; + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnSelectionChangeInternal(" + "aIMENotification={ mSelectionChangeData=%s }), mDestroyed=%s, " + "mSink=0x%p, mSinkMask=%s, mIsRecordingActionsWithoutLock=%s, " + "mComposition=%s", + this, mozilla::ToString(selectionChangeData).c_str(), + GetBoolName(mDestroyed), mSink.get(), + GetSinkMaskNameStr(mSinkMask).get(), + GetBoolName(mIsRecordingActionsWithoutLock), + ToString(mComposition).c_str())); + + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + mDeferNotifyingTSFUntilNextUpdate = false; + + // Assign the new selection change data to the pending selection change data + // because only the latest selection data is necessary. + // Note that this is necessary to update mSelectionForTSF. Therefore, even if + // neither TSF nor TIP wants selection change notifications, we need to + // store the selection information. + mPendingSelectionChangeData = Some(selectionChangeData); + + // Flush remaining pending notifications here if it's possible. + MaybeFlushPendingNotifications(); + + // If we're available, we should create native caret instead of IMEHandler + // because we may have some cache to do it. + // Note that if we have composition, we'll notified composition-updated + // later so that we don't need to create native caret in such case. + if (!IsHandlingCompositionInContent() && + IMEHandler::NeedsToCreateNativeCaret()) { + CreateNativeCaret(); + } + + return NS_OK; +} + +void TSFTextStore::NotifyTSFOfSelectionChange() { + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(!IsReadLocked()); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(mPendingSelectionChangeData.isSome()); + + // If selection range isn't actually changed, we don't need to notify TSF + // of this selection change. + if (mSelectionForTSF.isNothing()) { + MOZ_DIAGNOSTIC_ASSERT(!mIsInitializingSelectionForTSF, + "While mSelectionForTSF is being initialized, this " + "should not be called"); + mSelectionForTSF.emplace(*mPendingSelectionChangeData); + } else if (!mSelectionForTSF->SetSelection(*mPendingSelectionChangeData)) { + mPendingSelectionChangeData.reset(); + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::NotifyTSFOfSelectionChange(), " + "selection isn't actually changed.", + this)); + return; + } + + mPendingSelectionChangeData.reset(); + + if (!mSink || !(mSinkMask & TS_AS_SEL_CHANGE)) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfSelectionChange(), calling " + "ITextStoreACPSink::OnSelectionChange()...", + this)); + RefPtr<ITextStoreACPSink> sink = mSink; + sink->OnSelectionChange(); +} + +nsresult TSFTextStore::OnLayoutChangeInternal() { + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + NS_ENSURE_TRUE(mContext, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(mSink, NS_ERROR_FAILURE); + + mDeferNotifyingTSFUntilNextUpdate = false; + + nsresult rv = NS_OK; + + // We need to notify TSF of layout change even if the document is locked. + // So, don't use MaybeFlushPendingNotifications() for flushing pending + // layout change. + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnLayoutChangeInternal(), calling " + "NotifyTSFOfLayoutChange()...", + this)); + if (NS_WARN_IF(!NotifyTSFOfLayoutChange())) { + rv = NS_ERROR_FAILURE; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnLayoutChangeInternal(), calling " + "MaybeFlushPendingNotifications()...", + this)); + MaybeFlushPendingNotifications(); + + return rv; +} + +bool TSFTextStore::NotifyTSFOfLayoutChange() { + MOZ_ASSERT(!mDestroyed); + + // If we're waiting a query of layout information from TIP, it means that + // we've returned TS_E_NOLAYOUT error. + bool returnedNoLayoutError = mHasReturnedNoLayoutError || mWaitingQueryLayout; + + // If we returned TS_E_NOLAYOUT, TIP should query the computed layout again. + mWaitingQueryLayout = returnedNoLayoutError; + + // For avoiding to call this method again at unlocking the document during + // calls of OnLayoutChange(), reset mHasReturnedNoLayoutError. + mHasReturnedNoLayoutError = false; + + // Now, layout has been computed. We should notify mContentForTSF for + // making GetTextExt() and GetACPFromPoint() not return TS_E_NOLAYOUT. + if (mContentForTSF.isSome()) { + mContentForTSF->OnLayoutChanged(); + } + + if (IMEHandler::NeedsToCreateNativeCaret()) { + // If we're available, we should create native caret instead of IMEHandler + // because we may have some cache to do it. + CreateNativeCaret(); + } else { + // Now, the caret position is different from ours. Destroy the native caret + // if we've create it only for GetTextExt(). + IMEHandler::MaybeDestroyNativeCaret(); + } + + // This method should return true if either way succeeds. + bool ret = true; + + if (mSink) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "calling ITextStoreACPSink::OnLayoutChange()...", + this)); + RefPtr<ITextStoreACPSink> sink = mSink; + HRESULT hr = sink->OnLayoutChange(TS_LC_CHANGE, TEXTSTORE_DEFAULT_VIEW); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "called ITextStoreACPSink::OnLayoutChange()", + this)); + ret = SUCCEEDED(hr); + } + + // The layout change caused by composition string change should cause + // calling ITfContextOwnerServices::OnLayoutChange() too. + if (returnedNoLayoutError && mContext) { + RefPtr<ITfContextOwnerServices> service; + mContext->QueryInterface(IID_ITfContextOwnerServices, + getter_AddRefs(service)); + if (service) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "calling ITfContextOwnerServices::OnLayoutChange()...", + this)); + HRESULT hr = service->OnLayoutChange(); + ret = ret && SUCCEEDED(hr); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "called ITfContextOwnerServices::OnLayoutChange()", + this)); + } + } + + if (!mWidget || mWidget->Destroyed()) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "the widget is destroyed during calling OnLayoutChange()", + this)); + return ret; + } + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "the TSFTextStore instance is destroyed during calling " + "OnLayoutChange()", + this)); + return ret; + } + + // If we returned TS_E_NOLAYOUT again, we need another call of + // OnLayoutChange() later. So, let's wait a query from TIP. + if (mHasReturnedNoLayoutError) { + mWaitingQueryLayout = true; + } + + if (!mWaitingQueryLayout) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "succeeded notifying TIP of our layout change", + this)); + return ret; + } + + // If we believe that TIP needs to retry to retrieve our layout information + // later, we should call it with ::PostMessage() hack. + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "posing MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE for calling " + "OnLayoutChange() again...", + this)); + ::PostMessage(mWidget->GetWindowHandle(), MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE, + reinterpret_cast<WPARAM>(this), 0); + + return true; +} + +void TSFTextStore::NotifyTSFOfLayoutChangeAgain() { + // Don't notify TSF of layout change after destroyed. + if (mDestroyed) { + mWaitingQueryLayout = false; + return; + } + + // Before preforming this method, TIP has accessed our layout information by + // itself. In such case, we don't need to call OnLayoutChange() anymore. + if (!mWaitingQueryLayout) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "calling NotifyTSFOfLayoutChange()...", + this)); + NotifyTSFOfLayoutChange(); + + // If TIP didn't retrieved our layout information during a call of + // NotifyTSFOfLayoutChange(), it means that the TIP already gave up to + // retry to retrieve layout information or doesn't necessary it anymore. + // But don't forget that the call may have caused returning TS_E_NOLAYOUT + // error again. In such case we still need to call OnLayoutChange() later. + if (!mHasReturnedNoLayoutError && mWaitingQueryLayout) { + mWaitingQueryLayout = false; + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "called NotifyTSFOfLayoutChange() but TIP didn't retry to " + "retrieve the layout information", + this)); + } else { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "called NotifyTSFOfLayoutChange()", + this)); + } +} + +nsresult TSFTextStore::OnUpdateCompositionInternal() { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnUpdateCompositionInternal(), " + "mDestroyed=%s, mDeferNotifyingTSFUntilNextUpdate=%s", + this, GetBoolName(mDestroyed), + GetBoolName(mDeferNotifyingTSFUntilNextUpdate))); + + // There are nothing to do after destroyed. + if (mDestroyed) { + return NS_OK; + } + + // Update cached data now because all pending events have been handled now. + if (mContentForTSF.isSome()) { + mContentForTSF->OnCompositionEventsHandled(); + } + + // If composition is completely finished both in TSF/TIP and the focused + // editor which may be in a remote process, we can clear the cache and don't + // have it until starting next composition. + if (mComposition.isNothing() && !IsHandlingCompositionInContent()) { + mDeferClearingContentForTSF = false; + } + mDeferNotifyingTSFUntilNextUpdate = false; + MaybeFlushPendingNotifications(); + + // If we're available, we should create native caret instead of IMEHandler + // because we may have some cache to do it. + if (IMEHandler::NeedsToCreateNativeCaret()) { + CreateNativeCaret(); + } + + return NS_OK; +} + +nsresult TSFTextStore::OnMouseButtonEventInternal( + const IMENotification& aIMENotification) { + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // events. + return NS_OK; + } + + if (mMouseTrackers.IsEmpty()) { + return NS_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnMouseButtonEventInternal(" + "aIMENotification={ mEventMessage=%s, mOffset=%u, mCursorPos=%s, " + "mCharRect=%s, mButton=%s, mButtons=%s, mModifiers=%s })", + this, ToChar(aIMENotification.mMouseButtonEventData.mEventMessage), + aIMENotification.mMouseButtonEventData.mOffset, + ToString(aIMENotification.mMouseButtonEventData.mCursorPos).c_str(), + ToString(aIMENotification.mMouseButtonEventData.mCharRect).c_str(), + GetMouseButtonName(aIMENotification.mMouseButtonEventData.mButton), + GetMouseButtonsName(aIMENotification.mMouseButtonEventData.mButtons) + .get(), + GetModifiersName(aIMENotification.mMouseButtonEventData.mModifiers) + .get())); + + uint32_t offset = aIMENotification.mMouseButtonEventData.mOffset; + if (offset > static_cast<uint32_t>(LONG_MAX)) { + return NS_OK; + } + LayoutDeviceIntRect charRect = + aIMENotification.mMouseButtonEventData.mCharRect; + LayoutDeviceIntPoint cursorPos = + aIMENotification.mMouseButtonEventData.mCursorPos; + ULONG quadrant = 1; + if (charRect.Width() > 0) { + int32_t cursorXInChar = cursorPos.x - charRect.X(); + quadrant = cursorXInChar * 4 / charRect.Width(); + quadrant = (quadrant + 2) % 4; + } + ULONG edge = quadrant < 2 ? offset + 1 : offset; + DWORD buttonStatus = 0; + bool isMouseUp = + aIMENotification.mMouseButtonEventData.mEventMessage == eMouseUp; + if (!isMouseUp) { + switch (aIMENotification.mMouseButtonEventData.mButton) { + case MouseButton::ePrimary: + buttonStatus = MK_LBUTTON; + break; + case MouseButton::eMiddle: + buttonStatus = MK_MBUTTON; + break; + case MouseButton::eSecondary: + buttonStatus = MK_RBUTTON; + break; + } + } + if (aIMENotification.mMouseButtonEventData.mModifiers & MODIFIER_CONTROL) { + buttonStatus |= MK_CONTROL; + } + if (aIMENotification.mMouseButtonEventData.mModifiers & MODIFIER_SHIFT) { + buttonStatus |= MK_SHIFT; + } + for (size_t i = 0; i < mMouseTrackers.Length(); i++) { + MouseTracker& tracker = mMouseTrackers[i]; + if (!tracker.IsUsing() || tracker.Range().isNothing() || + !tracker.Range()->IsOffsetInRange(offset)) { + continue; + } + if (tracker.OnMouseButtonEvent(edge - tracker.Range()->StartOffset(), + quadrant, buttonStatus)) { + return NS_SUCCESS_EVENT_CONSUMED; + } + } + return NS_OK; +} + +void TSFTextStore::CreateNativeCaret() { + MOZ_ASSERT(!IMEHandler::IsA11yHandlingNativeCaret()); + + IMEHandler::MaybeDestroyNativeCaret(); + + // Don't create native caret after destroyed. + if (mDestroyed) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CreateNativeCaret(), mComposition=%s", this, + ToString(mComposition).c_str())); + + Maybe<Selection>& selectionForTSF = SelectionForTSF(); + if (MOZ_UNLIKELY(selectionForTSF.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "SelectionForTSF() failure", + this)); + return; + } + if (!selectionForTSF->HasRange() && mComposition.isNothing()) { + // If there is no selection range nor composition, then, we don't have a + // good position to show windows of TIP... + // XXX It seems that storing last caret rect and using it in this case might + // be better? + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::CreateNativeCaret() couludn't create native " + "caret due to no selection range", + this)); + return; + } + + WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, mWidget); + mWidget->InitEvent(queryCaretRectEvent); + + WidgetQueryContentEvent::Options options; + // XXX If this is called without composition and the selection isn't + // collapsed, is it OK? + int64_t caretOffset = selectionForTSF->HasRange() + ? selectionForTSF->MaxOffset() + : mComposition->StartOffset(); + if (mComposition.isSome()) { + // If there is a composition, use the relative query for deciding caret + // position because composition might be different place from that + // TSFTextStore assumes. + options.mRelativeToInsertionPoint = true; + caretOffset -= mComposition->StartOffset(); + } else if (!CanAccessActualContentDirectly()) { + // If TSF/TIP cannot access actual content directly, there may be pending + // text and/or selection changes which have not been notified TSF yet. + // Therefore, we should use the relative query from start of selection where + // TSFTextStore assumes since TSF/TIP computes the offset from our cached + // selection. + options.mRelativeToInsertionPoint = true; + caretOffset -= selectionForTSF->StartOffset(); + } + queryCaretRectEvent.InitForQueryCaretRect(caretOffset, options); + + DispatchEvent(queryCaretRectEvent); + if (NS_WARN_IF(queryCaretRectEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "eQueryCaretRect failure (offset=%lld)", + this, caretOffset)); + return; + } + + if (!IMEHandler::CreateNativeCaret(static_cast<nsWindow*>(mWidget.get()), + queryCaretRectEvent.mReply->mRect)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "IMEHandler::CreateNativeCaret() failure", + this)); + return; + } +} + +void TSFTextStore::CommitCompositionInternal(bool aDiscard) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(aDiscard=%s), " + "mSink=0x%p, mContext=0x%p, mComposition=%s", + this, GetBoolName(aDiscard), mSink.get(), mContext.get(), + ToString(mComposition).c_str())); + + // If the document is locked, TSF will fail to commit composition since + // TSF needs another document lock. So, let's put off the request. + // Note that TextComposition will commit composition in the focused editor + // with the latest composition string for web apps and waits asynchronous + // committing messages. Therefore, we can and need to perform this + // asynchronously. + if (IsReadLocked()) { + if (mDeferCommittingComposition || mDeferCancellingComposition) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(), " + "does nothing because already called and waiting unlock...", + this)); + return; + } + if (aDiscard) { + mDeferCancellingComposition = true; + } else { + mDeferCommittingComposition = true; + } + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(), " + "putting off to request to %s composition after unlocking the " + "document", + this, aDiscard ? "cancel" : "commit")); + return; + } + + if (mComposition.isSome() && aDiscard) { + LONG endOffset = mComposition->EndOffset(); + mComposition->SetData(EmptyString()); + // Note that don't notify TSF of text change after this is destroyed. + if (mSink && !mDestroyed) { + TS_TEXTCHANGE textChange; + textChange.acpStart = mComposition->StartOffset(); + textChange.acpOldEnd = endOffset; + textChange.acpNewEnd = mComposition->StartOffset(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::CommitCompositionInternal(), calling" + "mSink->OnTextChange(0, { acpStart=%ld, acpOldEnd=%ld, " + "acpNewEnd=%ld })...", + this, textChange.acpStart, textChange.acpOldEnd, + textChange.acpNewEnd)); + RefPtr<ITextStoreACPSink> sink = mSink; + sink->OnTextChange(0, &textChange); + } + } + // Terminate two contexts, the base context (mContext) and the top + // if the top context is not the same as the base context + RefPtr<ITfContext> context = mContext; + do { + if (context) { + RefPtr<ITfContextOwnerCompositionServices> services; + context->QueryInterface(IID_ITfContextOwnerCompositionServices, + getter_AddRefs(services)); + if (services) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(), " + "requesting TerminateComposition() for the context 0x%p...", + this, context.get())); + services->TerminateComposition(nullptr); + } + } + if (context != mContext) break; + if (mDocumentMgr) mDocumentMgr->GetTop(getter_AddRefs(context)); + } while (context != mContext); +} + +static bool GetCompartment(IUnknown* pUnk, const GUID& aID, + ITfCompartment** aCompartment) { + if (!pUnk) return false; + + RefPtr<ITfCompartmentMgr> compMgr; + pUnk->QueryInterface(IID_ITfCompartmentMgr, getter_AddRefs(compMgr)); + if (!compMgr) return false; + + return SUCCEEDED(compMgr->GetCompartment(aID, aCompartment)) && + (*aCompartment) != nullptr; +} + +// static +void TSFTextStore::SetIMEOpenState(bool aState) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::SetIMEOpenState(aState=%s)", GetBoolName(aState))); + + if (!sThreadMgr) { + return; + } + + RefPtr<ITfCompartment> comp = GetCompartmentForOpenClose(); + if (NS_WARN_IF(!comp)) { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetIMEOpenState() FAILED due to" + "no compartment available")); + return; + } + + VARIANT variant; + variant.vt = VT_I4; + variant.lVal = aState; + HRESULT hr = comp->SetValue(sClientId, &variant); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::SetIMEOpenState() FAILED due to " + "ITfCompartment::SetValue() failure, hr=0x%08lX", + hr)); + return; + } + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetIMEOpenState(), setting " + "0x%04lX to GUID_COMPARTMENT_KEYBOARD_OPENCLOSE...", + variant.lVal)); +} + +// static +bool TSFTextStore::GetIMEOpenState() { + if (!sThreadMgr) { + return false; + } + + RefPtr<ITfCompartment> comp = GetCompartmentForOpenClose(); + if (NS_WARN_IF(!comp)) { + return false; + } + + VARIANT variant; + ::VariantInit(&variant); + HRESULT hr = comp->GetValue(&variant); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetIMEOpenState() FAILED due to " + "ITfCompartment::GetValue() failure, hr=0x%08lX", + hr)); + return false; + } + // Until IME is open in this process, the result may be empty. + if (variant.vt == VT_EMPTY) { + return false; + } + if (NS_WARN_IF(variant.vt != VT_I4)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetIMEOpenState() FAILED due to " + "invalid result of ITfCompartment::GetValue()")); + ::VariantClear(&variant); + return false; + } + + return variant.lVal != 0; +} + +// static +void TSFTextStore::SetInputContext(nsWindow* aWidget, + const InputContext& aContext, + const InputContextAction& aAction) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::SetInputContext(aWidget=%p, " + "aContext=%s, aAction.mFocusChange=%s), " + "sEnabledTextStore(0x%p)={ mWidget=0x%p }, ThinksHavingFocus()=%s", + aWidget, mozilla::ToString(aContext).c_str(), + GetFocusChangeName(aAction.mFocusChange), sEnabledTextStore.get(), + sEnabledTextStore ? sEnabledTextStore->mWidget.get() : nullptr, + GetBoolName(ThinksHavingFocus()))); + + switch (aAction.mFocusChange) { + case InputContextAction::WIDGET_CREATED: + // If this is called when the widget is created, there is nothing to do. + return; + case InputContextAction::FOCUS_NOT_CHANGED: + case InputContextAction::MENU_LOST_PSEUDO_FOCUS: + if (NS_WARN_IF(!IsInTSFMode())) { + return; + } + // In these cases, `NOTIFY_IME_OF_FOCUS` won't be sent. Therefore, + // we need to reset text store for new state right now. + break; + default: + NS_WARNING_ASSERTION(IsInTSFMode(), + "Why is this called when TSF is disabled?"); + if (sEnabledTextStore) { + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + textStore->mInPrivateBrowsing = aContext.mInPrivateBrowsing; + textStore->SetInputScope(aContext.mHTMLInputType, + aContext.mHTMLInputMode); + if (aContext.mURI) { + nsAutoCString spec; + if (NS_SUCCEEDED(aContext.mURI->GetSpec(spec))) { + CopyUTF8toUTF16(spec, textStore->mDocumentURL); + } else { + textStore->mDocumentURL.Truncate(); + } + } else { + textStore->mDocumentURL.Truncate(); + } + } + return; + } + + // If focus isn't actually changed but the enabled state is changed, + // emulate the focus move. + if (!ThinksHavingFocus() && aContext.mIMEState.IsEditable()) { + if (!IMEHandler::GetFocusedWindow()) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::SetInputContent() gets called to enable IME, " + "but IMEHandler has not received focus notification")); + } else { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetInputContent() emulates focus for IME " + "state change")); + OnFocusChange(true, aWidget, aContext); + } + } else if (ThinksHavingFocus() && !aContext.mIMEState.IsEditable()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetInputContent() emulates blur for IME " + "state change")); + OnFocusChange(false, aWidget, aContext); + } +} + +// static +void TSFTextStore::MarkContextAsKeyboardDisabled(ITfContext* aContext) { + VARIANT variant_int4_value1; + variant_int4_value1.vt = VT_I4; + variant_int4_value1.lVal = 1; + + RefPtr<ITfCompartment> comp; + if (!GetCompartment(aContext, GUID_COMPARTMENT_KEYBOARD_DISABLED, + getter_AddRefs(comp))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::MarkContextAsKeyboardDisabled() failed" + "aContext=0x%p...", + aContext)); + return; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::MarkContextAsKeyboardDisabled(), setting " + "to disable context 0x%p...", + aContext)); + comp->SetValue(sClientId, &variant_int4_value1); +} + +// static +void TSFTextStore::MarkContextAsEmpty(ITfContext* aContext) { + VARIANT variant_int4_value1; + variant_int4_value1.vt = VT_I4; + variant_int4_value1.lVal = 1; + + RefPtr<ITfCompartment> comp; + if (!GetCompartment(aContext, GUID_COMPARTMENT_EMPTYCONTEXT, + getter_AddRefs(comp))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::MarkContextAsEmpty() failed" + "aContext=0x%p...", + aContext)); + return; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::MarkContextAsEmpty(), setting " + "to mark empty context 0x%p...", + aContext)); + comp->SetValue(sClientId, &variant_int4_value1); +} + +// static +void TSFTextStore::Initialize() { + MOZ_LOG(gIMELog, LogLevel::Info, ("TSFTextStore::Initialize() is called...")); + + if (sThreadMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED due to already initialized")); + return; + } + + const bool enableTsf = StaticPrefs::intl_tsf_enabled_AtStartup(); + MOZ_LOG(gIMELog, LogLevel::Info, + (" TSFTextStore::Initialize(), TSF is %s", + enableTsf ? "enabled" : "disabled")); + if (!enableTsf) { + return; + } + + RefPtr<ITfThreadMgr> threadMgr; + HRESULT hr = + ::CoCreateInstance(CLSID_TF_ThreadMgr, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfThreadMgr, getter_AddRefs(threadMgr)); + if (FAILED(hr) || !threadMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to " + "create the thread manager, hr=0x%08lX", + hr)); + return; + } + + hr = threadMgr->Activate(&sClientId); + if (FAILED(hr)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to activate, hr=0x%08lX", hr)); + return; + } + + RefPtr<ITfDocumentMgr> disabledDocumentMgr; + hr = threadMgr->CreateDocumentMgr(getter_AddRefs(disabledDocumentMgr)); + if (FAILED(hr) || !disabledDocumentMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a document manager for disabled mode, hr=0x%08lX", + hr)); + return; + } + + RefPtr<ITfContext> disabledContext; + DWORD editCookie = 0; + hr = disabledDocumentMgr->CreateContext( + sClientId, 0, nullptr, getter_AddRefs(disabledContext), &editCookie); + if (FAILED(hr) || !disabledContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a context for disabled mode, hr=0x%08lX", + hr)); + return; + } + + MarkContextAsKeyboardDisabled(disabledContext); + MarkContextAsEmpty(disabledContext); + + sThreadMgr = threadMgr; + sDisabledDocumentMgr = disabledDocumentMgr; + sDisabledContext = disabledContext; + + MOZ_LOG(gIMELog, LogLevel::Info, + (" TSFTextStore::Initialize(), sThreadMgr=0x%p, " + "sClientId=0x%08lX, sDisabledDocumentMgr=0x%p, sDisabledContext=%p", + sThreadMgr.get(), sClientId, sDisabledDocumentMgr.get(), + sDisabledContext.get())); +} + +// static +already_AddRefed<ITfThreadMgr> TSFTextStore::GetThreadMgr() { + RefPtr<ITfThreadMgr> threadMgr = sThreadMgr; + return threadMgr.forget(); +} + +// static +already_AddRefed<ITfMessagePump> TSFTextStore::GetMessagePump() { + static bool sInitialized = false; + if (!sThreadMgr) { + return nullptr; + } + if (sMessagePump) { + RefPtr<ITfMessagePump> messagePump = sMessagePump; + return messagePump.forget(); + } + // If it tried to retrieve ITfMessagePump from sThreadMgr but it failed, + // we shouldn't retry it at every message due to performance reason. + // Although this shouldn't occur actually. + if (sInitialized) { + return nullptr; + } + sInitialized = true; + + RefPtr<ITfMessagePump> messagePump; + HRESULT hr = sThreadMgr->QueryInterface(IID_ITfMessagePump, + getter_AddRefs(messagePump)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!messagePump)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetMessagePump() FAILED to " + "QI message pump from the thread manager, hr=0x%08lX", + hr)); + return nullptr; + } + sMessagePump = messagePump; + return messagePump.forget(); +} + +// static +already_AddRefed<ITfDisplayAttributeMgr> +TSFTextStore::GetDisplayAttributeMgr() { + RefPtr<ITfDisplayAttributeMgr> displayAttributeMgr; + if (sDisplayAttrMgr) { + displayAttributeMgr = sDisplayAttrMgr; + return displayAttributeMgr.forget(); + } + + HRESULT hr = ::CoCreateInstance( + CLSID_TF_DisplayAttributeMgr, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfDisplayAttributeMgr, getter_AddRefs(displayAttributeMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!displayAttributeMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetDisplayAttributeMgr() FAILED to create " + "a display attribute manager instance, hr=0x%08lX", + hr)); + return nullptr; + } + sDisplayAttrMgr = displayAttributeMgr; + return displayAttributeMgr.forget(); +} + +// static +already_AddRefed<ITfCategoryMgr> TSFTextStore::GetCategoryMgr() { + RefPtr<ITfCategoryMgr> categoryMgr; + if (sCategoryMgr) { + categoryMgr = sCategoryMgr; + return categoryMgr.forget(); + } + HRESULT hr = + ::CoCreateInstance(CLSID_TF_CategoryMgr, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfCategoryMgr, getter_AddRefs(categoryMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!categoryMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetCategoryMgr() FAILED to create " + "a category manager instance, hr=0x%08lX", + hr)); + return nullptr; + } + sCategoryMgr = categoryMgr; + return categoryMgr.forget(); +} + +// static +already_AddRefed<ITfCompartment> TSFTextStore::GetCompartmentForOpenClose() { + if (sCompartmentForOpenClose) { + RefPtr<ITfCompartment> compartment = sCompartmentForOpenClose; + return compartment.forget(); + } + + if (!sThreadMgr) { + return nullptr; + } + + RefPtr<ITfCompartmentMgr> compartmentMgr; + HRESULT hr = sThreadMgr->QueryInterface(IID_ITfCompartmentMgr, + getter_AddRefs(compartmentMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!compartmentMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetCompartmentForOpenClose() FAILED due to" + "sThreadMgr not having ITfCompartmentMgr, hr=0x%08lX", + hr)); + return nullptr; + } + + RefPtr<ITfCompartment> compartment; + hr = compartmentMgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_OPENCLOSE, + getter_AddRefs(compartment)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!compartment)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetCompartmentForOpenClose() FAILED due to" + "ITfCompartmentMgr::GetCompartment() failuere, hr=0x%08lX", + hr)); + return nullptr; + } + + sCompartmentForOpenClose = compartment; + return compartment.forget(); +} + +// static +already_AddRefed<ITfInputProcessorProfiles> +TSFTextStore::GetInputProcessorProfiles() { + RefPtr<ITfInputProcessorProfiles> inputProcessorProfiles; + if (sInputProcessorProfiles) { + inputProcessorProfiles = sInputProcessorProfiles; + return inputProcessorProfiles.forget(); + } + // XXX MSDN documents that ITfInputProcessorProfiles is available only on + // desktop apps. However, there is no known way to obtain + // ITfInputProcessorProfileMgr instance without ITfInputProcessorProfiles + // instance. + HRESULT hr = ::CoCreateInstance( + CLSID_TF_InputProcessorProfiles, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfInputProcessorProfiles, getter_AddRefs(inputProcessorProfiles)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!inputProcessorProfiles)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetInputProcessorProfiles() FAILED to create input " + "processor profiles, hr=0x%08lX", + hr)); + return nullptr; + } + sInputProcessorProfiles = inputProcessorProfiles; + return inputProcessorProfiles.forget(); +} + +// static +void TSFTextStore::Terminate() { + MOZ_LOG(gIMELog, LogLevel::Info, ("TSFTextStore::Terminate()")); + + TSFStaticSink::Shutdown(); + + sDisplayAttrMgr = nullptr; + sCategoryMgr = nullptr; + sEnabledTextStore = nullptr; + sDisabledDocumentMgr = nullptr; + sDisabledContext = nullptr; + sCompartmentForOpenClose = nullptr; + sInputProcessorProfiles = nullptr; + sClientId = 0; + if (sThreadMgr) { + sThreadMgr->Deactivate(); + sThreadMgr = nullptr; + sMessagePump = nullptr; + sKeystrokeMgr = nullptr; + } +} + +// static +bool TSFTextStore::ProcessRawKeyMessage(const MSG& aMsg) { + if (!sThreadMgr) { + return false; // not in TSF mode + } + static bool sInitialized = false; + if (!sKeystrokeMgr) { + // If it tried to retrieve ITfKeystrokeMgr from sThreadMgr but it failed, + // we shouldn't retry it at every keydown nor keyup due to performance + // reason. Although this shouldn't occur actually. + if (sInitialized) { + return false; + } + sInitialized = true; + RefPtr<ITfKeystrokeMgr> keystrokeMgr; + HRESULT hr = sThreadMgr->QueryInterface(IID_ITfKeystrokeMgr, + getter_AddRefs(keystrokeMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!keystrokeMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::ProcessRawKeyMessage() FAILED to " + "QI keystroke manager from the thread manager, hr=0x%08lX", + hr)); + return false; + } + sKeystrokeMgr = keystrokeMgr.forget(); + } + + if (aMsg.message == WM_KEYDOWN) { + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + if (textStore) { + textStore->OnStartToHandleKeyMessage(); + if (NS_WARN_IF(textStore != sEnabledTextStore)) { + // Let's handle the key message with new focused TSFTextStore. + textStore = sEnabledTextStore; + } + } + AutoRestore<const MSG*> savePreviousKeyMsg(sHandlingKeyMsg); + AutoRestore<bool> saveKeyEventDispatched(sIsKeyboardEventDispatched); + sHandlingKeyMsg = &aMsg; + sIsKeyboardEventDispatched = false; + BOOL eaten; + RefPtr<ITfKeystrokeMgr> keystrokeMgr = sKeystrokeMgr; + HRESULT hr = keystrokeMgr->TestKeyDown(aMsg.wParam, aMsg.lParam, &eaten); + if (FAILED(hr) || !sKeystrokeMgr || !eaten) { + return false; + } + hr = keystrokeMgr->KeyDown(aMsg.wParam, aMsg.lParam, &eaten); + if (textStore) { + textStore->OnEndHandlingKeyMessage(!!eaten); + } + return SUCCEEDED(hr) && + (eaten || !sKeystrokeMgr || sIsKeyboardEventDispatched); + } + if (aMsg.message == WM_KEYUP) { + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + if (textStore) { + textStore->OnStartToHandleKeyMessage(); + if (NS_WARN_IF(textStore != sEnabledTextStore)) { + // Let's handle the key message with new focused TSFTextStore. + textStore = sEnabledTextStore; + } + } + AutoRestore<const MSG*> savePreviousKeyMsg(sHandlingKeyMsg); + AutoRestore<bool> saveKeyEventDispatched(sIsKeyboardEventDispatched); + sHandlingKeyMsg = &aMsg; + sIsKeyboardEventDispatched = false; + BOOL eaten; + RefPtr<ITfKeystrokeMgr> keystrokeMgr = sKeystrokeMgr; + HRESULT hr = keystrokeMgr->TestKeyUp(aMsg.wParam, aMsg.lParam, &eaten); + if (FAILED(hr) || !sKeystrokeMgr || !eaten) { + return false; + } + hr = keystrokeMgr->KeyUp(aMsg.wParam, aMsg.lParam, &eaten); + if (textStore) { + textStore->OnEndHandlingKeyMessage(!!eaten); + } + return SUCCEEDED(hr) && + (eaten || !sKeystrokeMgr || sIsKeyboardEventDispatched); + } + return false; +} + +// static +void TSFTextStore::ProcessMessage(nsWindow* aWindow, UINT aMessage, + WPARAM& aWParam, LPARAM& aLParam, + MSGResult& aResult) { + switch (aMessage) { + case WM_IME_SETCONTEXT: + // If a windowless plugin had focus and IME was handled on it, composition + // window was set the position. After that, even in TSF mode, WinXP keeps + // to use composition window at the position if the active IME is not + // aware TSF. For avoiding this issue, we need to hide the composition + // window here. + if (aWParam) { + aLParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + } + break; + case WM_ENTERIDLE: + // When an modal dialog such as a file picker is open, composition + // should be committed because IME might be used on it. + if (!IsComposingOn(aWindow)) { + break; + } + CommitComposition(false); + break; + case MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE: { + TSFTextStore* maybeTextStore = reinterpret_cast<TSFTextStore*>(aWParam); + if (maybeTextStore == sEnabledTextStore) { + RefPtr<TSFTextStore> textStore(maybeTextStore); + textStore->NotifyTSFOfLayoutChangeAgain(); + } + break; + } + } +} + +// static +bool TSFTextStore::IsIMM_IMEActive() { + return TSFStaticSink::IsIMM_IMEActive(); +} + +// static +bool TSFTextStore::IsMSJapaneseIMEActive() { + return TSFStaticSink::IsMSJapaneseIMEActive(); +} + +// static +bool TSFTextStore::IsGoogleJapaneseInputActive() { + return TSFStaticSink::IsGoogleJapaneseInputActive(); +} + +// static +bool TSFTextStore::IsATOKActive() { return TSFStaticSink::IsATOKActive(); } + +/****************************************************************************** + * TSFTextStore::Content + *****************************************************************************/ + +const nsDependentSubstring TSFTextStore::Content::GetSelectedText() const { + if (NS_WARN_IF(mSelection.isNothing())) { + return nsDependentSubstring(); + } + return GetSubstring(static_cast<uint32_t>(mSelection->StartOffset()), + static_cast<uint32_t>(mSelection->Length())); +} + +const nsDependentSubstring TSFTextStore::Content::GetSubstring( + uint32_t aStart, uint32_t aLength) const { + return nsDependentSubstring(mText, aStart, aLength); +} + +void TSFTextStore::Content::ReplaceSelectedTextWith(const nsAString& aString) { + if (NS_WARN_IF(mSelection.isNothing())) { + return; + } + ReplaceTextWith(mSelection->StartOffset(), mSelection->Length(), aString); +} + +inline uint32_t FirstDifferentCharOffset(const nsAString& aStr1, + const nsAString& aStr2) { + MOZ_ASSERT(aStr1 != aStr2); + uint32_t i = 0; + uint32_t minLength = std::min(aStr1.Length(), aStr2.Length()); + for (; i < minLength && aStr1[i] == aStr2[i]; i++) { + /* nothing to do */ + } + return i; +} + +void TSFTextStore::Content::ReplaceTextWith(LONG aStart, LONG aLength, + const nsAString& aReplaceString) { + MOZ_ASSERT(aStart >= 0); + MOZ_ASSERT(aLength >= 0); + const nsDependentSubstring replacedString = GetSubstring( + static_cast<uint32_t>(aStart), static_cast<uint32_t>(aLength)); + if (aReplaceString != replacedString) { + uint32_t firstDifferentOffset = mMinModifiedOffset.valueOr(UINT32_MAX); + if (mComposition.isSome()) { + // Emulate text insertion during compositions, because during a + // composition, editor expects the whole composition string to + // be sent in eCompositionChange, not just the inserted part. + // The actual eCompositionChange will be sent in SetSelection + // or OnUpdateComposition. + MOZ_ASSERT(aStart >= mComposition->StartOffset()); + MOZ_ASSERT(aStart + aLength <= mComposition->EndOffset()); + mComposition->ReplaceData( + static_cast<uint32_t>(aStart - mComposition->StartOffset()), + static_cast<uint32_t>(aLength), aReplaceString); + // TIP may set composition string twice or more times during a document + // lock. Therefore, we should compute the first difference offset with + // mLastComposition. + if (mLastComposition.isNothing()) { + firstDifferentOffset = mComposition->StartOffset(); + } else if (mComposition->DataRef() != mLastComposition->DataRef()) { + firstDifferentOffset = + mComposition->StartOffset() + + FirstDifferentCharOffset(mComposition->DataRef(), + mLastComposition->DataRef()); + // The previous change to the composition string is canceled. + if (mMinModifiedOffset.isSome() && + mMinModifiedOffset.value() >= + static_cast<uint32_t>(mComposition->StartOffset()) && + mMinModifiedOffset.value() < firstDifferentOffset) { + mMinModifiedOffset = Some(firstDifferentOffset); + } + } else if (mMinModifiedOffset.isSome() && + mMinModifiedOffset.value() < static_cast<uint32_t>(LONG_MAX) && + mComposition->IsOffsetInRange( + static_cast<long>(mMinModifiedOffset.value()))) { + // The previous change to the composition string is canceled. + firstDifferentOffset = mComposition->EndOffset(); + mMinModifiedOffset = Some(firstDifferentOffset); + } + mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets()); + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::Content::ReplaceTextWith(aStart=%ld, " + "aLength=%ld, aReplaceString=\"%s\"), mComposition=%s, " + "mLastComposition=%s, mMinModifiedOffset=%s, " + "firstDifferentOffset=%u", + this, aStart, aLength, GetEscapedUTF8String(aReplaceString).get(), + ToString(mComposition).c_str(), ToString(mLastComposition).c_str(), + ToString(mMinModifiedOffset).c_str(), firstDifferentOffset)); + } else { + firstDifferentOffset = + static_cast<uint32_t>(aStart) + + FirstDifferentCharOffset(aReplaceString, replacedString); + } + mMinModifiedOffset = + mMinModifiedOffset.isNothing() + ? Some(firstDifferentOffset) + : Some(std::min(mMinModifiedOffset.value(), firstDifferentOffset)); + mText.Replace(static_cast<uint32_t>(aStart), static_cast<uint32_t>(aLength), + aReplaceString); + } + // Selection should be collapsed at the end of the inserted string. + mSelection = Some(TSFTextStore::Selection(static_cast<uint32_t>(aStart) + + aReplaceString.Length())); +} + +void TSFTextStore::Content::StartComposition( + ITfCompositionView* aCompositionView, const PendingAction& aCompStart, + bool aPreserveSelection) { + MOZ_ASSERT(aCompositionView); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(aCompStart.mType == PendingAction::Type::eCompositionStart); + + mComposition.reset(); // Avoid new crash in the beta and nightly channels. + mComposition.emplace( + aCompositionView, aCompStart.mSelectionStart, + GetSubstring(static_cast<uint32_t>(aCompStart.mSelectionStart), + static_cast<uint32_t>(aCompStart.mSelectionLength))); + mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets()); + if (!aPreserveSelection) { + // XXX Do we need to set a new writing-mode here when setting a new + // selection? Currently, we just preserve the existing value. + WritingMode writingMode = + mSelection.isNothing() ? WritingMode() : mSelection->WritingModeRef(); + mSelection = Some(TSFTextStore::Selection(mComposition->StartOffset(), + mComposition->Length(), false, + writingMode)); + } +} + +void TSFTextStore::Content::RestoreCommittedComposition( + ITfCompositionView* aCompositionView, + const PendingAction& aCanceledCompositionEnd) { + MOZ_ASSERT(aCompositionView); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(aCanceledCompositionEnd.mType == + PendingAction::Type::eCompositionEnd); + MOZ_ASSERT( + GetSubstring( + static_cast<uint32_t>(aCanceledCompositionEnd.mSelectionStart), + static_cast<uint32_t>(aCanceledCompositionEnd.mData.Length())) == + aCanceledCompositionEnd.mData); + + // Restore the committed string as composing string. + mComposition.reset(); // Avoid new crash in the beta and nightly channels. + mComposition.emplace(aCompositionView, + aCanceledCompositionEnd.mSelectionStart, + aCanceledCompositionEnd.mData); + mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets()); +} + +void TSFTextStore::Content::EndComposition(const PendingAction& aCompEnd) { + MOZ_ASSERT(mComposition.isSome()); + MOZ_ASSERT(aCompEnd.mType == PendingAction::Type::eCompositionEnd); + + if (mComposition.isNothing()) { + return; // Avoid new crash in the beta and nightly channels. + } + + mSelection = Some(TSFTextStore::Selection(mComposition->StartOffset() + + aCompEnd.mData.Length())); + mComposition.reset(); +} + +/****************************************************************************** + * TSFTextStore::MouseTracker + *****************************************************************************/ + +TSFTextStore::MouseTracker::MouseTracker() : mCookie(kInvalidCookie) {} + +HRESULT +TSFTextStore::MouseTracker::Init(TSFTextStore* aTextStore) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::Init(aTextStore=0x%p), " + "aTextStore->mMouseTrackers.Length()=%zu", + this, aTextStore, aTextStore->mMouseTrackers.Length())); + + if (&aTextStore->mMouseTrackers.LastElement() != this) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::Init() FAILED due to " + "this is not the last element of mMouseTrackers", + this)); + return E_FAIL; + } + if (aTextStore->mMouseTrackers.Length() > kInvalidCookie) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::Init() FAILED due to " + "no new cookie available", + this)); + return E_FAIL; + } + MOZ_ASSERT(!aTextStore->mMouseTrackers.IsEmpty(), + "This instance must be in TSFTextStore::mMouseTrackers"); + mCookie = static_cast<DWORD>(aTextStore->mMouseTrackers.Length() - 1); + return S_OK; +} + +HRESULT +TSFTextStore::MouseTracker::AdviseSink(TSFTextStore* aTextStore, + ITfRangeACP* aTextRange, + ITfMouseSink* aMouseSink) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::AdviseSink(aTextStore=0x%p, " + "aTextRange=0x%p, aMouseSink=0x%p), mCookie=%ld, mSink=0x%p", + this, aTextStore, aTextRange, aMouseSink, mCookie, mSink.get())); + MOZ_ASSERT(mCookie != kInvalidCookie, "This hasn't been initalized?"); + + if (mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to already being used", + this)); + return E_FAIL; + } + + MOZ_ASSERT(mRange.isNothing()); + + LONG start = 0, length = 0; + HRESULT hr = aTextRange->GetExtent(&start, &length); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to failure of ITfRangeACP::GetExtent()", + this)); + return hr; + } + + if (start < 0 || length <= 0 || start + length > LONG_MAX) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to odd result of ITfRangeACP::GetExtent(), " + "start=%ld, length=%ld", + this, start, length)); + return E_INVALIDARG; + } + + nsAutoString textContent; + if (NS_WARN_IF(!aTextStore->GetCurrentText(textContent))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to failure of TSFTextStore::GetCurrentText()", + this)); + return E_FAIL; + } + + if (textContent.Length() <= static_cast<uint32_t>(start) || + textContent.Length() < static_cast<uint32_t>(start + length)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to out of range, start=%ld, length=%ld, " + "textContent.Length()=%zu", + this, start, length, textContent.Length())); + return E_INVALIDARG; + } + + mRange.emplace(start, start + length); + + mSink = aMouseSink; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink(), " + "succeeded, mRange=%s, textContent.Length()=%zu", + this, ToString(mRange).c_str(), textContent.Length())); + return S_OK; +} + +void TSFTextStore::MouseTracker::UnadviseSink() { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::UnadviseSink(), " + "mCookie=%ld, mSink=0x%p, mRange=%s", + this, mCookie, mSink.get(), ToString(mRange).c_str())); + mSink = nullptr; + mRange.reset(); +} + +bool TSFTextStore::MouseTracker::OnMouseButtonEvent(ULONG aEdge, + ULONG aQuadrant, + DWORD aButtonStatus) { + MOZ_ASSERT(IsUsing(), "The caller must check before calling OnMouseEvent()"); + + BOOL eaten = FALSE; + RefPtr<ITfMouseSink> sink = mSink; + HRESULT hr = sink->OnMouseEvent(aEdge, aQuadrant, aButtonStatus, &eaten); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::OnMouseEvent(aEdge=%ld, " + "aQuadrant=%ld, aButtonStatus=0x%08lX), hr=0x%08lX, eaten=%s", + this, aEdge, aQuadrant, aButtonStatus, hr, GetBoolName(!!eaten))); + + return SUCCEEDED(hr) && eaten; +} + +#ifdef DEBUG +// static +bool TSFTextStore::CurrentKeyboardLayoutHasIME() { + RefPtr<ITfInputProcessorProfiles> inputProcessorProfiles = + TSFTextStore::GetInputProcessorProfiles(); + if (!inputProcessorProfiles) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED due to " + "there is no input processor profiles instance")); + return false; + } + RefPtr<ITfInputProcessorProfileMgr> profileMgr; + HRESULT hr = inputProcessorProfiles->QueryInterface( + IID_ITfInputProcessorProfileMgr, getter_AddRefs(profileMgr)); + if (FAILED(hr) || !profileMgr) { + // On Windows Vista or later, ImmIsIME() API always returns true. + // If we failed to obtain the profile manager, we cannot know if current + // keyboard layout has IME. + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED to query " + "ITfInputProcessorProfileMgr")); + return false; + } + + TF_INPUTPROCESSORPROFILE profile; + hr = profileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile); + if (hr == S_FALSE) { + return false; // not found or not active + } + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED to retreive " + "active profile")); + return false; + } + return (profile.dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR); +} +#endif // #ifdef DEBUG + +} // namespace widget +} // namespace mozilla |