diff options
Diffstat (limited to 'widget/windows/TSFTextStore.cpp')
-rw-r--r-- | widget/windows/TSFTextStore.cpp | 6423 |
1 files changed, 6423 insertions, 0 deletions
diff --git a/widget/windows/TSFTextStore.cpp b/widget/windows/TSFTextStore.cpp new file mode 100644 index 000000000..fb0505aa3 --- /dev/null +++ b/widget/windows/TSFTextStore.cpp @@ -0,0 +1,6423 @@ +/* -*- 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 <olectl.h> +#include <algorithm> + +#include "nscore.h" +#include "nsWindow.h" +#include "nsPrintfCString.h" +#include "WinIMEHandler.h" +#include "WinUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/WindowsVersion.h" +#include "nsIXULRuntime.h" + +namespace mozilla { +namespace widget { + +static const char* kPrefNameEnableTSF = "intl.tsf.enable"; + +/** + * 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(" + */ + +LazyLogModule sTextStoreLog("nsTextStoreWidgets"); + +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) +{ + nsAutoCString 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: + 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* +GetIMEEnabledName(IMEState::Enabled aIMEEnabled) +{ + switch (aIMEEnabled) { + case IMEState::DISABLED: + return "DISABLED"; + case IMEState::ENABLED: + return "ENABLED"; + case IMEState::PASSWORD: + return "PASSWORD"; + case IMEState::PLUGIN: + return "PLUGIN"; + default: + return "Invalid"; + } +} + +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"; + default: + return "Unknown"; + } +} + +static nsCString +GetCLSIDNameStr(REFCLSID aCLSID) +{ + LPOLESTR str = nullptr; + HRESULT hr = ::StringFromCLSID(aCLSID, &str); + if (FAILED(hr) || !str || !str[0]) { + return EmptyCString(); + } + + nsAutoCString 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 EmptyCString(); + } + + return NS_ConvertUTF16toUTF8(str); +} + +static nsCString +GetGUIDNameStrWithTable(REFGUID aGUID) +{ +#define RETURN_GUID_NAME(aNamedGUID) \ + if (IsEqualGUID(aGUID, aNamedGUID)) { \ + return NS_LITERAL_CSTRING(#aNamedGUID); \ + } + + RETURN_GUID_NAME(GUID_PROP_INPUTSCOPE) + 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 EmptyCString(); + } + + nsAutoString key(L"Interface\\"); + key += str; + + nsAutoCString result; + wchar_t buf[256]; + if (WinUtils::GetRegistryKey(HKEY_CLASSES_ROOT, key.get(), nullptr, + buf, sizeof(buf))) { + 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) +{ + nsAutoCString 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 char* +GetActiveSelEndName(TsActiveSelEnd aSelEnd) +{ + return aSelEnd == TS_AE_NONE ? "TS_AE_NONE" : + aSelEnd == TS_AE_START ? "TS_AE_START" : + aSelEnd == TS_AE_END ? "TS_AE_END" : "Unknown"; +} + +static const nsCString +GetLockFlagNameStr(DWORD aLockFlags) +{ + nsAutoCString 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 NS_LITERAL_CSTRING("TF_CT_NONE"); + 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 NS_LITERAL_CSTRING("TF_LS_NONE"); + case TF_LS_SOLID: + return NS_LITERAL_CSTRING("TF_LS_SOLID"); + case TF_LS_DOT: + return NS_LITERAL_CSTRING("TF_LS_DOT"); + case TF_LS_DASH: + return NS_LITERAL_CSTRING("TF_LS_DASH"); + case TF_LS_SQUIGGLE: + return NS_LITERAL_CSTRING("TF_LS_SQUIGGLE"); + 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 NS_LITERAL_CSTRING("TF_ATTR_INPUT"); + case TF_ATTR_TARGET_CONVERTED: + return NS_LITERAL_CSTRING("TF_ATTR_TARGET_CONVERTED"); + case TF_ATTR_CONVERTED: + return NS_LITERAL_CSTRING("TF_ATTR_CONVERTED"); + case TF_ATTR_TARGET_NOTCONVERTED: + return NS_LITERAL_CSTRING("TF_ATTR_TARGET_NOTCONVERTED"); + case TF_ATTR_INPUT_ERROR: + return NS_LITERAL_CSTRING("TF_ATTR_INPUT_ERROR"); + case TF_ATTR_FIXEDCONVERTED: + return NS_LITERAL_CSTRING("TF_ATTR_FIXEDCONVERTED"); + case TF_ATTR_OTHER: + return NS_LITERAL_CSTRING("TF_ATTR_OTHER"); + default: { + return nsPrintfCString("Unknown(%08X)", static_cast<int32_t>(aAttr)); + } + } +} + +static nsCString +GetDisplayAttrStr(const TF_DISPLAYATTRIBUTE& aDispAttr) +{ + nsAutoCString 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 WidgetMouseEventBase::eLeftButton: + return "LeftButton"; + case WidgetMouseEventBase::eMiddleButton: + return "MiddleButton"; + case WidgetMouseEventBase::eRightButton: + 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 NS_LITERAL_CSTRING("no buttons"); + } + nsAutoCString names; + if (aButtons & WidgetMouseEventBase::eLeftButtonFlag) { + names = "LeftButton"; + } + if (aButtons & WidgetMouseEventBase::eRightButtonFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "RightButton"; + } + if (aButtons & WidgetMouseEventBase::eMiddleButtonFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "MiddleButton"; + } + if (aButtons & WidgetMouseEventBase::e4thButtonFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "4thButton"; + } + if (aButtons & WidgetMouseEventBase::e5thButtonFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "5thButton"; + } + return names; +} + +static nsCString +GetModifiersName(Modifiers aModifiers) +{ + if (aModifiers == MODIFIER_NONE) { + return NS_LITERAL_CSTRING("no modifiers"); + } + nsAutoCString 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; + } + if (aModifiers & MODIFIER_OS) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_OS; + } + return names; +} + +class GetWritingModeName : public nsAutoCString +{ +public: + 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"); + } +}; + +/******************************************************************/ +/* InputScopeImpl */ +/******************************************************************/ + +class InputScopeImpl final : public ITfInputScope +{ + ~InputScopeImpl() {} + +public: + InputScopeImpl(const nsTArray<InputScope>& aList) + : mInputScopes(aList) + { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p InputScopeImpl()", this)); + } + + 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) { + sInstance = new TSFStaticSink(); + } + 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() + { + 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); + } + + bool EnsureInitActiveTIPKeyboard(); + + /**************************************************************************** + * Japanese TIP + ****************************************************************************/ + + // Note that TIP name may depend on the language of the environment. + // For example, some TIP may use localized name for its target language + // environment but English name for the others. + + bool IsMSJapaneseIMEActive() const + { + // FYI: Name of MS-IME for Japanese is same as MS-IME for Korean. + // Therefore, we need to check the langid too. + return mLangID == 0x411 && + (mActiveTIPKeyboardDescription.EqualsLiteral("Microsoft IME") || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"Microsoft \xC785\xB825\xAE30")) || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8F6F\x8F93\x5165\x6CD5")) || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8EDF\x8F38\x5165\x6CD5"))); + } + + bool IsMSOfficeJapaneseIME2010Active() const + { + // {54EDCC94-1524-4BB1-9FB7-7BABE4F4CA64} + static const GUID kGUID = { + 0x54EDCC94, 0x1524, 0x4BB1, + { 0x9F, 0xB7, 0x7B, 0xAB, 0xE4, 0xF4, 0xCA, 0x64 } + }; + return mActiveTIPGUID == kGUID; + } + + bool IsATOKActive() const + { + // FYI: Name of ATOK includes the release year like "ATOK 2015". + return StringBeginsWith(mActiveTIPKeyboardDescription, + NS_LITERAL_STRING("ATOK ")); + } + + bool IsATOK2011Active() const + { + // {F9C24A5C-8A53-499D-9572-93B2FF582115} + static const GUID kGUID = { + 0xF9C24A5C, 0x8A53, 0x499D, + { 0x95, 0x72, 0x93, 0xB2, 0xFF, 0x58, 0x21, 0x15 } + }; + return mActiveTIPGUID == kGUID; + } + + bool IsATOK2012Active() const + { + // {1DE01562-F445-401B-B6C3-E5B18DB79461} + static const GUID kGUID = { + 0x1DE01562, 0xF445, 0x401B, + { 0xB6, 0xC3, 0xE5, 0xB1, 0x8D, 0xB7, 0x94, 0x61 } + }; + return mActiveTIPGUID == kGUID; + } + + bool IsATOK2013Active() const + { + // {3C4DB511-189A-4168-B6EA-BFD0B4C85615} + static const GUID kGUID = { + 0x3C4DB511, 0x189A, 0x4168, + { 0xB6, 0xEA, 0xBF, 0xD0, 0xB4, 0xC8, 0x56, 0x15 } + }; + return mActiveTIPGUID == kGUID; + } + + bool IsATOK2014Active() const + { + // {4EF33B79-6AA9-4271-B4BF-9321C279381B} + static const GUID kGUID = { + 0x4EF33B79, 0x6AA9, 0x4271, + { 0xB4, 0xBF, 0x93, 0x21, 0xC2, 0x79, 0x38, 0x1B } + }; + return mActiveTIPGUID == kGUID; + } + + bool IsATOK2015Active() const + { + // {EAB4DC00-CE2E-483D-A86A-E6B99DA9599A} + static const GUID kGUID = { + 0xEAB4DC00, 0xCE2E, 0x483D, + { 0xA8, 0x6A, 0xE6, 0xB9, 0x9D, 0xA9, 0x59, 0x9A } + }; + return mActiveTIPGUID == kGUID; + } + + bool IsATOK2016Active() const + { + // {0B557B4C-5740-4110-A60A-1493FA10BF2B} + static const GUID kGUID = { + 0x0B557B4C, 0x5740, 0x4110, + { 0xA6, 0x0A, 0x14, 0x93, 0xFA, 0x10, 0xBF, 0x2B } + }; + return mActiveTIPGUID == kGUID; + } + + // Note that ATOK 2011 - 2016 refers native caret position for deciding its + // popup window position. + bool IsATOKReferringNativeCaretActive() const + { + return IsATOKActive() && + (IsATOK2011Active() || IsATOK2012Active() || IsATOK2013Active() || + IsATOK2014Active() || IsATOK2015Active() || IsATOK2016Active()); + } + + /**************************************************************************** + * Traditional Chinese TIP + ****************************************************************************/ + + bool IsMSChangJieActive() const + { + return mActiveTIPKeyboardDescription.EqualsLiteral("Microsoft ChangJie") || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8F6F\x4ED3\x9889")) || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8EDF\x5009\x9821")); + } + + bool IsMSQuickQuickActive() const + { + return mActiveTIPKeyboardDescription.EqualsLiteral("Microsoft Quick") || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8F6F\x901F\x6210")) || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8EDF\x901F\x6210")); + } + + bool IsFreeChangJieActive() const + { + // FYI: The TIP name is misspelled... + return mActiveTIPKeyboardDescription.EqualsLiteral("Free CangJie IME 10"); + } + + bool IsEasyChangjeiActive() const + { + return + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING( + u"\x4E2D\x6587 (\x7E41\x9AD4) - \x6613\x9821\x8F38\x5165\x6CD5")); + } + + /**************************************************************************** + * Simplified Chinese TIP + ****************************************************************************/ + + bool IsMSPinyinActive() const + { + return mActiveTIPKeyboardDescription.EqualsLiteral("Microsoft Pinyin") || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8F6F\x62FC\x97F3")) || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8EDF\x62FC\x97F3")); + } + + bool IsMSWubiActive() const + { + return mActiveTIPKeyboardDescription.EqualsLiteral("Microsoft Wubi") || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8F6F\x4E94\x7B14")) || + mActiveTIPKeyboardDescription.Equals( + NS_LITERAL_STRING(u"\x5FAE\x8EDF\x4E94\x7B46")); + } + +public: // ITfInputProcessorProfileActivationSink + STDMETHODIMP OnActivated(DWORD, LANGID, REFCLSID, REFGUID, REFGUID, + HKL, DWORD); + +private: + TSFStaticSink(); + virtual ~TSFStaticSink() {} + + void Destroy(); + + void GetTIPDescription(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile, nsAString& aDescription); + bool IsTIPCategoryKeyboard(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile); + + // 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 + GUID mActiveTIPGUID; + + static StaticRefPtr<TSFStaticSink> sInstance; +}; + +StaticRefPtr<TSFStaticSink> TSFStaticSink::sInstance; + +TSFStaticSink::TSFStaticSink() + : 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFStaticSink::Init() FAILED to get ITfSource " + "instance (0x%08X)", 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFStaticSink::Init() FAILED to install " + "ITfInputProcessorProfileActivationSink (0x%08X)", this, hr)); + return false; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFStaticSink::Init(), " + "mIPProfileCookie=0x%08X", + this, mIPProfileCookie)); + return true; +} + +void +TSFStaticSink::Destroy() +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFStaticSink::Shutdown() " + "mIPProfileCookie=0x%08X", + this, mIPProfileCookie)); + + if (mIPProfileCookie != TF_INVALID_COOKIE) { + RefPtr<ITfSource> source; + HRESULT hr = + mThreadMgr->QueryInterface(IID_ITfSource, getter_AddRefs(source)); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFStaticSink::Shutdown() FAILED to get " + "ITfSource instance (0x%08X)", this, hr)); + } else { + hr = source->UnadviseSink(mIPProfileCookie); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::Shutdown() FAILED to uninstall " + "ITfInputProcessorProfileActivationSink (0x%08X)", + 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; + mActiveTIPGUID = guidProfile; + mLangID = langid; + mIsIMM_IME = IsIMM_IME(hkl); + GetTIPDescription(rclsid, mLangID, guidProfile, + mActiveTIPKeyboardDescription); + // Notify IMEHandler of changing active keyboard layout. + IMEHandler::OnKeyboardLayoutChanged(); + } + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFStaticSink::OnActivated(dwProfileType=%s (0x%08X), " + "langid=0x%08X, rclsid=%s, catid=%s, guidProfile=%s, hkl=0x%08X, " + "dwFlags=0x%08X (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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get input processor profile manager, hr=0x%08X", this, hr)); + return false; + } + + TF_INPUTPROCESSORPROFILE profile; + hr = profileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile); + if (hr == S_FALSE) { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get active keyboard layout profile due to no active profile, " + "hr=0x%08X", this, hr)); + // XXX Should we call OnActivated() with arguments like non-TIP in this + // case? + return false; + } + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get active TIP keyboard, hr=0x%08X", this, hr)); + return false; + } + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFStaticSink::InitActiveTIPDescription() FAILED " + "due to GetLanguageProfileDescription() failure, hr=0x%08X", + 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFStaticSink::IsTIPCategoryKeyboard(), FAILED " + "to get language profiles enumerator, hr=0x%08X", 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<ITfDocumentMgr> TSFTextStore::sDisabledDocumentMgr; +StaticRefPtr<ITfContext> TSFTextStore::sDisabledContext; +StaticRefPtr<ITfInputProcessorProfiles> TSFTextStore::sInputProcessorProfiles; +StaticRefPtr<TSFTextStore> TSFTextStore::sEnabledTextStore; +DWORD TSFTextStore::sClientId = 0; + +bool TSFTextStore::sCreateNativeCaretForLegacyATOK = false; +bool TSFTextStore::sDoNotReturnNoLayoutErrorToATOKOfCompositionString = false; +bool TSFTextStore::sDoNotReturnNoLayoutErrorToMSSimplifiedTIP = false; +bool TSFTextStore::sDoNotReturnNoLayoutErrorToMSTraditionalTIP = false; +bool TSFTextStore::sDoNotReturnNoLayoutErrorToFreeChangJie = false; +bool TSFTextStore::sDoNotReturnNoLayoutErrorToEasyChangjei = false; +bool TSFTextStore::sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar = false; +bool TSFTextStore::sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret = false; +bool TSFTextStore::sHackQueryInsertForMSSimplifiedTIP = false; +bool TSFTextStore::sHackQueryInsertForMSTraditionalTIP = false; + +#define TEXTSTORE_DEFAULT_VIEW (1) + +TSFTextStore::TSFTextStore() + : mEditCookie(0) + , mSinkMask(0) + , mLock(0) + , mLockQueued(0) + , mHandlingKeyMessage(0) + , mContentForTSF(mComposition, mSelectionForTSF) + , mRequestedAttrValues(false) + , mIsRecordingActionsWithoutLock(false) + , mHasReturnedNoLayoutError(false) + , mWaitingQueryLayout(false) + , mPendingDestroy(false) + , mDeferClearingContentForTSF(false) + , mNativeCaretIsCreated(false) + , mDeferNotifyingTSF(false) + , mDeferCommittingComposition(false) + , mDeferCancellingComposition(false) + , mDestroyed(false) + , mBeingDestroyed(false) +{ + for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) { + mRequestedAttrs[i] = false; + } + + // We hope that 5 or more actions don't occur at once. + mPendingActions.SetCapacity(5); + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::TSFTextStore() SUCCEEDED", this)); +} + +TSFTextStore::~TSFTextStore() +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore instance is destroyed", this)); +} + +bool +TSFTextStore::Init(nsWindowBase* aWidget, + const InputContext& aContext) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::Init(aWidget=0x%p)", + this, aWidget)); + + if (NS_WARN_IF(!aWidget) || NS_WARN_IF(aWidget->Destroyed())) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED due to being initialized with " + "destroyed widget", + this)); + return false; + } + + TSFStaticSink::GetInstance()->EnsureInitActiveTIPKeyboard(); + + if (mDocumentMgr) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED due to already initialized", + this)); + return false; + } + + mWidget = aWidget; + if (NS_WARN_IF(!mWidget)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED " + "due to aWidget->GetTextEventDispatcher() failure", this)); + return false; + } + + SetInputScope(aContext.mHTMLInputType, aContext.mHTMLInputInputmode); + + // 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfDocumentMgr " + "(0x%08X)", this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create the context " + "(0x%08X)", this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to push the context (0x%08X)", + this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::Init() succeeded: " + "mDocumentMgr=0x%p, mContext=0x%p, mEditCookie=0x%08X", + this, mDocumentMgr.get(), mContext.get(), mEditCookie)); + + return true; +} + +void +TSFTextStore::Destroy() +{ + if (mBeingDestroyed) { + return; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::Destroy(), mLock=%s, " + "mComposition.IsComposing()=%s, mHandlingKeyMessage=%u", + this, GetLockFlagNameStr(mLock).get(), + GetBoolName(mComposition.IsComposing()), + 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. + 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.IsComposing()) { + CommitCompositionInternal(false); + } + + if (mSink) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::Destroy() succeeded", this)); +} + +void +TSFTextStore::ReleaseTSFObjects() +{ + MOZ_ASSERT(!mHandlingKeyMessage); + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::ReleaseTSFObjects()", this)); + + 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::ReleaseTSFObjects(), " + "removing a mouse tracker...", + this)); + mMouseTrackers.Clear(); + } + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to the null punk", + this)); + return E_UNEXPECTED; + } + + if (IID_ITextStoreACPSink != riid) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseSink(punk=0x%p), mSink=0x%p", + this, punk, mSink.get())); + + if (!punk) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseSink() FAILED due to the null punk", + this)); + return E_INVALIDARG; + } + if (!mSink) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RequestLock() FAILED due to " + "any sink not stored", this)); + return E_FAIL; + } + if (mDestroyed && + (!mContentForTSF.IsInitialized() || mSelectionForTSF.IsDirty())) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", + this, GetLockFlagNameStr(mLock).get())); + DidLockGranted(); + while (mLockQueued) { + mLock = mLockQueued; + mLockQueued = 0; + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p Locking for the request in the queue (%s) >>>>>>>>>>>>>>" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", + this, GetLockFlagNameStr(mLock).get())); + sink->OnLockGranted(mLock); + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", + this, GetLockFlagNameStr(mLock).get())); + DidLockGranted(); + } + + // The document is now completely unlocked. + mLock = 0; + + MaybeFlushPendingNotifications(); + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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.Clear(); + 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()) { + mDeferNotifyingTSF = 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.Clear(); + mHasReturnedNoLayoutError = false; + return; + } + + RefPtr<nsWindowBase> kungFuDeathGrip(mWidget); + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sTextStoreLog, 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::COMPOSITION_START: { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing COMPOSITION_START={ mSelectionStart=%d, " + "mSelectionLength=%d }, mDestroyed=%s", + this, action.mSelectionStart, action.mSelectionLength, + GetBoolName(mDestroyed))); + + if (mDestroyed) { + MOZ_LOG(sTextStoreLog, 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, mWidget); + mWidget->InitEvent(selectionSet); + selectionSet.mOffset = static_cast<uint32_t>(action.mSelectionStart); + selectionSet.mLength = static_cast<uint32_t>(action.mSelectionLength); + selectionSet.mReversed = false; + DispatchEvent(selectionSet); + if (!selectionSet.mSucceeded) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "dispatching compositionstart event...", this)); + WidgetEventTime eventTime = mWidget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->StartComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositionstart event, " + "IsComposingInContent()=%s", + this, GetBoolName(!IsComposingInContent()))); + mDeferClearingContentForTSF = !IsComposingInContent(); + } + if (!mWidget || mWidget->Destroyed()) { + break; + } + break; + } + case PendingAction::COMPOSITION_UPDATE: { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing COMPOSITION_UPDATE={ mData=\"%s\", " + "mRanges=0x%p, mRanges->Length()=%d }", + 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to setting pending composition... " + "IsComposingInContent()=%s", + this, GetBoolName(IsComposingInContent()))); + mDeferClearingContentForTSF = !IsComposingInContent(); + } else { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "dispatching compositionchange event...", this)); + WidgetEventTime eventTime = mWidget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->FlushPendingComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositionchange event, " + "IsComposingInContent()=%s", + this, GetBoolName(IsComposingInContent()))); + mDeferClearingContentForTSF = !IsComposingInContent(); + } + // Be aware, the mWidget might already have been destroyed. + } + break; + } + case PendingAction::COMPOSITION_END: { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing COMPOSITION_END={ 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. + // In this case, we should not clear mContentForTSFuntil we notify + // the IME of the composition update. + mDeferClearingContentForTSF = true; + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions(), " + "dispatching compositioncommit event...", this)); + WidgetEventTime eventTime = mWidget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->CommitComposition(status, &action.mData, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositioncommit event, " + "IsComposingInContent()=%s", + this, GetBoolName(IsComposingInContent()))); + mDeferClearingContentForTSF = !IsComposingInContent(); + } + break; + } + case PendingAction::SET_SELECTION: { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing SET_SELECTION={ mSelectionStart=%d, " + "mSelectionLength=%d, mSelectionReversed=%s }, " + "mDestroyed=%s", + this, action.mSelectionStart, action.mSelectionLength, + GetBoolName(action.mSelectionReversed), + GetBoolName(mDestroyed))); + + if (mDestroyed) { + MOZ_LOG(sTextStoreLog, LogLevel::Warning, + ("0x%p TSFTextStore::FlushPendingActions() " + "IGNORED pending selectionset due to already destroyed", + this)); + break; + } + + WidgetSelectionEvent selectionSet(true, eSetSelection, mWidget); + selectionSet.mOffset = + static_cast<uint32_t>(action.mSelectionStart); + selectionSet.mLength = + static_cast<uint32_t>(action.mSelectionLength); + selectionSet.mReversed = action.mSelectionReversed; + break; + } + default: + MOZ_CRASH("unexpected action type"); + } + + if (mWidget && !mWidget->Destroyed()) { + continue; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::FlushPendingActions(), " + "qutting since the mWidget has gone", this)); + break; + } + mPendingActions.Clear(); +} + +void +TSFTextStore::MaybeFlushPendingNotifications() +{ + if (IsReadLocked()) { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "putting off flushing pending notifications due to being the " + "document locked...", this)); + return; + } + + if (mDeferCommittingComposition) { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::CommitCompositionInternal(false)...", this)); + mDeferCommittingComposition = mDeferCancellingComposition = false; + CommitCompositionInternal(false); + } else if (mDeferCancellingComposition) { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::CommitCompositionInternal(true)...", this)); + mDeferCommittingComposition = mDeferCancellingComposition = false; + CommitCompositionInternal(true); + } + + if (mDeferNotifyingTSF) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "does nothing because this has already destroyed completely...", this)); + return; + } + + if (!mDeferClearingContentForTSF && mContentForTSF.IsInitialized()) { + mContentForTSF.Clear(); + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "mContentForTSF is cleared", 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.IsInitialized()) { + if (mPendingTextChangeData.IsValid()) { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfTextChange()...", this)); + NotifyTSFOfTextChange(); + } + if (mPendingSelectionChangeData.IsValid()) { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfSelectionChange()...", this)); + NotifyTSFOfSelectionChange(); + } + } + + if (mHasReturnedNoLayoutError) { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfLayoutChange()...", this)); + NotifyTSFOfLayoutChange(); + } +} + +STDMETHODIMP +TSFTextStore::GetStatus(TS_STATUS* pdcs) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetStatus(pdcs=0x%p)", this, pdcs)); + + if (!pdcs) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetStatus() FAILED due to null pdcs", this)); + return E_INVALIDARG; + } + pdcs->dwDynamicFlags = 0; + // 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::QueryInsert(acpTestStart=%ld, " + "acpTestEnd=%ld, cch=%lu, pacpResultStart=0x%p, pacpResultEnd=0x%p)", + this, acpTestStart, acpTestEnd, cch, acpTestStart, acpTestEnd)); + + if (!pacpResultStart || !pacpResultEnd) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::QueryInsert() FAILED due to " + "the null argument", this)); + return E_INVALIDARG; + } + + if (acpTestStart < 0 || acpTestStart > acpTestEnd) { + MOZ_LOG(sTextStoreLog, 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 + const TSFStaticSink* kSink = TSFStaticSink::GetInstance(); + if (IsWin8OrLater() && !mComposition.IsComposing() && + ((sHackQueryInsertForMSTraditionalTIP && + (kSink->IsMSChangJieActive() || kSink->IsMSQuickQuickActive())) || + (sHackQueryInsertForMSSimplifiedTIP && + (kSink->IsMSPinyinActive() || kSink->IsMSWubiActive())))) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to not locked", + this)); + return TS_E_NOLOCK; + } + if (!ulCount || !pSelection || !pcFetched) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "unsupported selection", this)); + return TS_E_NOSELECTION; + } + + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "SelectionForTSFRef() failure", this)); + return E_FAIL; + } + *pSelection = selectionForTSF.ACP(); + *pcFetched = 1; + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetSelection() succeeded", this)); + return S_OK; +} + +bool +TSFTextStore::IsComposingInContent() const +{ + if (!mDispatcher) { + return false; + } + if (!mDispatcher->IsInNativeInputTransaction()) { + return false; + } + return mDispatcher->IsComposing(); +} + +TSFTextStore::Content& +TSFTextStore::ContentForTSFRef() +{ + // This should be called when the document is locked or the content hasn't + // been abandoned yet. + if (NS_WARN_IF(!IsReadLocked() && !mContentForTSF.IsInitialized())) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSFRef(), FAILED, due to " + "called wrong timing, IsReadLocked()=%s, " + "mContentForTSF.IsInitialized()=%s", + this, GetBoolName(IsReadLocked()), + GetBoolName(mContentForTSF.IsInitialized()))); + mContentForTSF.Clear(); + return mContentForTSF; + } + + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSFRef(), FAILED, due to " + "SelectionForTSFRef() failure", this)); + mContentForTSF.Clear(); + return mContentForTSF; + } + + if (!mContentForTSF.IsInitialized()) { + nsAutoString text; + if (NS_WARN_IF(!GetCurrentText(text))) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSFRef(), FAILED, due to " + "GetCurrentText() failure", this)); + mContentForTSF.Clear(); + return mContentForTSF; + } + + mContentForTSF.Init(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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::ContentForTSFRef(): " + "mContentForTSF={ mText=\"%s\" (Length()=%u), " + "mLastCompositionString=\"%s\" (Length()=%u), " + "mMinTextModifiedOffset=%u }", + this, mContentForTSF.Text().Length() <= 40 ? + GetEscapedUTF8String(mContentForTSF.Text()).get() : "<omitted>", + mContentForTSF.Text().Length(), + GetEscapedUTF8String(mContentForTSF.LastCompositionString()).get(), + mContentForTSF.LastCompositionString().Length(), + mContentForTSF.MinTextModifiedOffset())); + + return mContentForTSF; +} + +bool +TSFTextStore::CanAccessActualContentDirectly() const +{ + if (!mContentForTSF.IsInitialized() || mSelectionForTSF.IsDirty()) { + 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.IsValid()) { + return true; + } + + return mSelectionForTSF.EqualsExceptDirection(mPendingSelectionChangeData); +} + +bool +TSFTextStore::GetCurrentText(nsAString& aTextContent) +{ + if (mContentForTSF.IsInitialized()) { + aTextContent = mContentForTSF.Text(); + return true; + } + + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(mWidget && !mWidget->Destroyed()); + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::GetCurrentText(): " + "retrieving text from the content...", this)); + + WidgetQueryContentEvent queryText(true, eQueryTextContent, mWidget); + queryText.InitForQueryTextContent(0, UINT32_MAX); + mWidget->InitEvent(queryText); + DispatchEvent(queryText); + if (NS_WARN_IF(!queryText.mSucceeded)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetCurrentText(), FAILED, due to " + "eQueryTextContent failure", this)); + aTextContent.Truncate(); + return false; + } + + aTextContent = queryText.mReply.mString; + return true; +} + +TSFTextStore::Selection& +TSFTextStore::SelectionForTSFRef() +{ + if (mSelectionForTSF.IsDirty()) { + 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_CRASH(); + } + + WidgetQueryContentEvent querySelection(true, eQuerySelectedText, mWidget); + mWidget->InitEvent(querySelection); + DispatchEvent(querySelection); + if (NS_WARN_IF(!querySelection.mSucceeded)) { + return mSelectionForTSF; + } + + mSelectionForTSF.SetSelection(querySelection.mReply.mOffset, + querySelection.mReply.mString.Length(), + querySelection.mReply.mReversed, + querySelection.GetWritingMode()); + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::SelectionForTSFRef(): " + "acpStart=%d, acpEnd=%d (length=%d), reverted=%s", + this, mSelectionForTSF.StartOffset(), mSelectionForTSF.EndOffset(), + mSelectionForTSF.Length(), + GetBoolName(mSelectionForTSF.IsReversed()))); + + 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(sTextStoreLog, LogLevel::Debug)) { + LONG start = 0, length = 0; + hr = GetRangeExtent(aRange, &start, &length); + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::GetDisplayAttribute(): " + "GetDisplayAttribute range=%ld-%ld (hr=%s)", + this, start - mComposition.mStart, + start - mComposition.mStart + length, + GetCommonReturnValueName(hr))); + } + + VARIANT propValue; + ::VariantInit(&propValue); + hr = aAttrProperty->GetValue(TfEditCookie(mEditCookie), aRange, &propValue); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfProperty::GetValue() failed", this)); + return hr; + } + if (VT_I4 != propValue.vt) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfProperty::GetValue() returns non-VT_I4 value", this)); + ::VariantClear(&propValue); + return E_FAIL; + } + + NS_ENSURE_TRUE(sCategoryMgr, E_FAIL); + GUID guid; + hr = sCategoryMgr->GetGUID(DWORD(propValue.lVal), &guid); + ::VariantClear(&propValue); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfCategoryMgr::GetGUID() failed", this)); + return hr; + } + + NS_ENSURE_TRUE(sDisplayAttrMgr, E_FAIL); + RefPtr<ITfDisplayAttributeInfo> info; + hr = sDisplayAttrMgr->GetDisplayAttributeInfo(guid, getter_AddRefs(info), + nullptr); + if (FAILED(hr) || !info) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfDisplayAttributeMgr::GetDisplayAttributeInfo() failed", this)); + return hr; + } + + hr = info->GetAttributeInfo(aResult); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfDisplayAttributeInfo::GetAttributeInfo() failed", this)); + return hr; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::GetDisplayAttribute() succeeded: " + "Result={ %s }", this, GetDisplayAttrStr(*aResult).get())); + return S_OK; +} + +HRESULT +TSFTextStore::RestartCompositionIfNecessary(ITfRange* aRangeNew) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary(" + "aRangeNew=0x%p), mComposition.mView=0x%p", + this, aRangeNew, mComposition.mView.get())); + + if (!mComposition.IsComposing()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED " + "due to no composition view", this)); + return E_FAIL; + } + + HRESULT hr; + RefPtr<ITfCompositionView> pComposition(mComposition.mView); + RefPtr<ITfRange> composingRange(aRangeNew); + if (!composingRange) { + hr = pComposition->GetRange(getter_AddRefs(composingRange)); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED " + "due to GetRangeExtent() failure", this)); + return hr; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary(), " + "range=%ld-%ld, mComposition={ mStart=%ld, mString.Length()=%lu }", + this, compStart, compStart + compLength, mComposition.mStart, + mComposition.mString.Length())); + + if (mComposition.mStart != compStart || + mComposition.mString.Length() != (ULONG)compLength) { + // 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(pComposition, composingRange); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() " + "FAILED due to RestartComposition() failure", this)); + return hr; + } + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() succeeded", + this)); + return S_OK; +} + +HRESULT +TSFTextStore::RestartComposition(ITfCompositionView* aCompositionView, + ITfRange* aNewRange) +{ + Selection& selectionForTSF = SelectionForTSFRef(); + + LONG newStart, newLength; + HRESULT hr = GetRangeExtent(aNewRange, &newStart, &newLength); + LONG newEnd = newStart + newLength; + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartComposition(aCompositionView=0x%p, " + "aNewRange=0x%p { newStart=%d, newLength=%d }), " + "mComposition={ mStart=%d, mCompositionString.Length()=%d }, " + "selectionForTSF={ IsDirty()=%s, StartOffset()=%d, Length()=%d }", + this, aCompositionView, aNewRange, newStart, newLength, + mComposition.mStart, mComposition.mString.Length(), + GetBoolName(selectionForTSF.IsDirty()), + selectionForTSF.StartOffset(), selectionForTSF.Length())); + + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to SelectionForTSFRef() failure", this)); + return E_FAIL; + } + + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, 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 >= mComposition.EndOffset() || newEnd <= mComposition.mStart) { + RecordCompositionEndAction(); + RecordCompositionStartAction(aCompositionView, newStart, newLength, true); + return S_OK; + } + + // 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 = mComposition; + Selection oldSelection = selectionForTSF; + + // Commit only the part of composition. + LONG keepComposingStartOffset = std::max(mComposition.mStart, newStart); + LONG keepComposingEndOffset = std::min(mComposition.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(mComposition.mString); + commitString.Cut(keepComposingStartOffset - mComposition.mStart, + keepComposingLength); + // Update the composition string. + Content& contentForTSF = ContentForTSFRef(); + if (!contentForTSF.IsInitialized()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to ContentForTSFRef() failure", this)); + return E_FAIL; + } + contentForTSF.ReplaceTextWith(mComposition.mStart, + mComposition.mString.Length(), + commitString); + // Record a compositionupdate action for commit the part of composing string. + PendingAction* action = LastOrNewPendingCompositionUpdate(); + action->mData = mComposition.mString; + 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 = + uint32_t(oldComposition.mStart + 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.mString, + keepComposingStartOffset - oldComposition.mStart, + keepComposingLength)); + selectionForTSF = oldSelection; + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartComposition() succeeded, " + "mComposition={ mStart=%d, mCompositionString.Length()=%d }, " + "selectionForTSF={ IsDirty()=%s, StartOffset()=%d, Length()=%d }", + this, mComposition.mStart, mComposition.mString.Length(), + GetBoolName(selectionForTSF.IsDirty()), + selectionForTSF.StartOffset(), selectionForTSF.Length())); + + 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, uint8_t& 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionUpdateAction(), " + "mComposition={ mView=0x%p, mStart=%d, mString=\"%s\" " + "(Length()=%d) }", + this, mComposition.mView.get(), mComposition.mStart, + GetEscapedUTF8String(mComposition.mString).get(), + mComposition.mString.Length())); + + if (!mComposition.IsComposing()) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to mContext->GetProperty() failure", this)); + return FAILED(hr) ? hr : E_FAIL; + } + + RefPtr<ITfRange> composingRange; + hr = mComposition.mView->GetRange(getter_AddRefs(composingRange)); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "FAILED due to mComposition.mView->GetRange() failure", this)); + return hr; + } + + RefPtr<IEnumTfRanges> enumRanges; + hr = attrPropetry->EnumRanges(TfEditCookie(mEditCookie), + getter_AddRefs(enumRanges), composingRange); + if (FAILED(hr) || !enumRanges) { + MOZ_LOG(sTextStoreLog, 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. + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to SelectionForTSFRef() failure", this)); + return E_FAIL; + } + + PendingAction* action = LastOrNewPendingCompositionUpdate(); + action->mData = mComposition.mString; + // 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.mStart), + mComposition.EndOffset()); + LONG end = std::max(std::min(rangeStart + rangeLength, + mComposition.EndOffset()), + mComposition.mStart); + LONG length = end - start; + if (length < 0) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "ignores invalid range (%d-%d)", + this, rangeStart - mComposition.mStart, + rangeStart - mComposition.mStart + rangeLength)); + continue; + } + if (!length) { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "ignores a range due to outside of the composition or empty " + "(%d-%d)", + this, rangeStart - mComposition.mStart, + rangeStart - mComposition.mStart + rangeLength)); + continue; + } + + TextRange newRange; + newRange.mStartOffset = uint32_t(start - mComposition.mStart); + // The end of the last range in the array is + // always kept at the end of composition + newRange.mEndOffset = mComposition.mString.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.IsCollapsed() && action->mRanges->Length() == 1) { + TextRange& range = action->mRanges->ElementAt(0); + LONG start = selectionForTSF.MinOffset(); + LONG end = selectionForTSF.MaxOffset(); + if ((LONG)range.mStartOffset == start - mComposition.mStart && + (LONG)range.mEndOffset == end - mComposition.mStart && + 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.MaxOffset() - mComposition.mStart); + + // 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "succeeded", this)); + + return S_OK; +} + +HRESULT +TSFTextStore::SetSelectionInternal(const TS_SELECTION_ACP* pSelection, + bool aDispatchCompositionChangeEvent) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::SetSelectionInternal(pSelection={ " + "acpStart=%ld, acpEnd=%ld, style={ ase=%s, fInterimChar=%s} }, " + "aDispatchCompositionChangeEvent=%s), mComposition.IsComposing()=%s", + this, pSelection->acpStart, pSelection->acpEnd, + GetActiveSelEndName(pSelection->style.ase), + GetBoolName(pSelection->style.fInterimChar), + GetBoolName(aDispatchCompositionChangeEvent), + GetBoolName(mComposition.IsComposing()))); + + MOZ_ASSERT(IsReadWriteLocked()); + + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "SelectionForTSFRef() failure", 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() Succeeded but " + "did nothing because the selection range isn't changing", this)); + selectionForTSF.SetSelection(*pSelection); + return S_OK; + } + + if (mComposition.IsComposing()) { + if (aDispatchCompositionChangeEvent) { + HRESULT hr = RestartCompositionIfNecessary(); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "RestartCompositionIfNecessary() failure", this)); + return hr; + } + } + if (pSelection->acpStart < mComposition.mStart || + pSelection->acpEnd > mComposition.EndOffset()) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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.IsInitialized() && + 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(sTextStoreLog, 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::SET_SELECTION; + 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::SetSelection(ulCount=%lu, pSelection=%p { " + "acpStart=%ld, acpEnd=%ld, style={ ase=%s, fInterimChar=%s } }), " + "mComposition.IsComposing()=%s", + this, ulCount, pSelection, + pSelection ? pSelection->acpStart : 0, + pSelection ? pSelection->acpEnd : 0, + pSelection ? GetActiveSelEndName(pSelection->style.ase) : "", + pSelection ? GetBoolName(pSelection->style.fInterimChar) : "", + GetBoolName(mComposition.IsComposing()))); + + if (!IsReadWriteLocked()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "not locked (read-write)", this)); + return TS_E_NOLOCK; + } + if (ulCount != 1) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "trying setting multiple selection", this)); + return E_INVALIDARG; + } + if (!pSelection) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "SetSelectionInternal() failure", this)); + } else { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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={ mStart=%ld, " + "mString.Length()=%lu, IsComposing()=%s }", + this, acpStart, acpEnd, pchPlain, cchPlainReq, pcchPlainOut, + prgRunInfo, ulRunInfoReq, pulRunInfoOut, pacpNext, + mComposition.mStart, mComposition.mString.Length(), + GetBoolName(mComposition.IsComposing()))); + + if (!IsReadLocked()) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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; + } + + Content& contentForTSF = ContentForTSFRef(); + if (!contentForTSF.IsInitialized()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "ContentForTSFRef() failure", this)); + return E_FAIL; + } + if (contentForTSF.Text().Length() < static_cast<uint32_t>(acpStart)) { + MOZ_LOG(sTextStoreLog, 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.Text().Length() < static_cast<uint32_t>(acpEnd)) { + MOZ_LOG(sTextStoreLog, 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.Text().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.Text().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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::SetText(dwFlags=%s, acpStart=%ld, " + "acpEnd=%ld, pchText=0x%p \"%s\", cch=%lu, pChange=0x%p), " + "mComposition.IsComposing()=%s", + this, dwFlags == TS_ST_CORRECTION ? "TS_ST_CORRECTION" : + "not-specified", + acpStart, acpEnd, pchText, + pchText && cch ? + GetEscapedUTF8String(pchText, cch).get() : "", + cch, pChange, GetBoolName(mComposition.IsComposing()))); + + // 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::SetText() FAILED due to " + "InsertTextAtSelectionInternal() failure", this)); + return E_FAIL; + } + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::InsertEmbedded() called " + "but not supported (E_NOTIMPL)", this)); + + // embedded objects are not supported + return E_NOTIMPL; +} + +void +TSFTextStore::SetInputScope(const nsString& aHTMLInputType, + const nsString& aHTMLInputInputMode) +{ + mInputScopes.Clear(); + if (aHTMLInputType.IsEmpty() || aHTMLInputType.EqualsLiteral("text")) { + if (aHTMLInputInputMode.EqualsLiteral("url")) { + mInputScopes.AppendElement(IS_URL); + } else if (aHTMLInputInputMode.EqualsLiteral("email")) { + mInputScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS); + } else if (aHTMLInputType.EqualsLiteral("tel")) { + mInputScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER); + mInputScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER); + } else if (aHTMLInputType.EqualsLiteral("numeric")) { + mInputScopes.AppendElement(IS_NUMBER); + } + return; + } + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html + if (aHTMLInputType.EqualsLiteral("url")) { + mInputScopes.AppendElement(IS_URL); + } else if (aHTMLInputType.EqualsLiteral("search")) { + mInputScopes.AppendElement(IS_SEARCH); + } else if (aHTMLInputType.EqualsLiteral("email")) { + mInputScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS); + } else if (aHTMLInputType.EqualsLiteral("password")) { + mInputScopes.AppendElement(IS_PASSWORD); + } else if (aHTMLInputType.EqualsLiteral("datetime") || + aHTMLInputType.EqualsLiteral("datetime-local")) { + mInputScopes.AppendElement(IS_DATE_FULLDATE); + mInputScopes.AppendElement(IS_TIME_FULLTIME); + } else if (aHTMLInputType.EqualsLiteral("date") || + aHTMLInputType.EqualsLiteral("month") || + aHTMLInputType.EqualsLiteral("week")) { + mInputScopes.AppendElement(IS_DATE_FULLDATE); + } else if (aHTMLInputType.EqualsLiteral("time")) { + mInputScopes.AppendElement(IS_TIME_FULLTIME); + } else if (aHTMLInputType.EqualsLiteral("tel")) { + mInputScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER); + mInputScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER); + } else if (aHTMLInputType.EqualsLiteral("number")) { + mInputScopes.AppendElement(IS_NUMBER); + } +} + +int32_t +TSFTextStore::GetRequestedAttrIndex(const TS_ATTRID& aAttrID) +{ + if (IsEqualGUID(aAttrID, GUID_PROP_INPUTSCOPE)) { + return eInputScope; + } + 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 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::HandleRequestAttrs(aFlags=%s, " + "aFilterCount=%u)", + 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + (" 0x%p TSFTextStore::FindNextAttrTransition() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + MOZ_LOG(sTextStoreLog, 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; +} + +STDMETHODIMP +TSFTextStore::RetrieveRequestedAttrs(ULONG ulCount, + TS_ATTRVAL* paAttrVals, + ULONG* pcFetched) +{ + if (!pcFetched || !paAttrVals) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() FAILED due to " + "not enough count ulCount=%u, expectedCount=%u", + this, ulCount, expectedCount)); + return E_INVALIDARG; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() called " + "ulCount=%d, mRequestedAttrValues=%s", + this, ulCount, GetBoolName(mRequestedAttrValues))); + + 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(sTextStoreLog, 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 eTextVerticalWriting: { + Selection& selectionForTSF = SelectionForTSFRef(); + paAttrVals[count].varValue.vt = VT_BOOL; + paAttrVals[count].varValue.boolVal = + selectionForTSF.GetWritingMode().IsVertical() ? VARIANT_TRUE : + VARIANT_FALSE; + break; + } + case eTextOrientation: { + Selection& selectionForTSF = SelectionForTSFRef(); + paAttrVals[count].varValue.vt = VT_I4; + paAttrVals[count].varValue.lVal = + selectionForTSF.GetWritingMode().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(sTextStoreLog, 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; +} + +STDMETHODIMP +TSFTextStore::GetEndACP(LONG* pacp) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetEndACP(pacp=0x%p)", this, pacp)); + + if (!IsReadLocked()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "not locked (read)", this)); + return TS_E_NOLOCK; + } + + if (!pacp) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + Content& contentForTSF = ContentForTSFRef(); + if (!contentForTSF.IsInitialized()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "ContentForTSFRef() failure", this)); + return E_FAIL; + } + *pacp = static_cast<LONG>(contentForTSF.Text().Length()); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetActiveView(TsViewCookie* pvcView) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetActiveView(pvcView=0x%p)", + this, pvcView)); + + if (!pvcView) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetActiveView() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + *pvcView = TEXTSTORE_DEFAULT_VIEW; + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetACPFromPoint(pvcView=%d, pt=%p (x=%d, " + "y=%d), dwFlags=%s, pacp=%p, mDeferNotifyingTSF=%s, " + "mWaitingQueryLayout=%s", + this, vcView, pt, pt ? pt->x : 0, pt ? pt->y : 0, + GetACPFromPointFlagName(dwFlags).get(), pacp, + GetBoolName(mDeferNotifyingTSF), GetBoolName(mWaitingQueryLayout))); + + if (!IsReadLocked()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "not locked (read)", this)); + return TS_E_NOLOCK; + } + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "called with invalid view", this)); + return E_INVALIDARG; + } + + if (!pt) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "null pt", this)); + return E_INVALIDARG; + } + + if (!pacp) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "null pacp", this)); + return E_INVALIDARG; + } + + mWaitingQueryLayout = false; + + if (mDestroyed || mContentForTSF.IsLayoutChanged()) { + MOZ_LOG(sTextStoreLog, 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 charAtPt(true, eQueryCharacterAtPoint, mWidget); + mWidget->InitEvent(charAtPt, &ourPt); + + // FYI: WidgetQueryContentEvent may cause flushing pending layout and it + // may cause focus change or something. + RefPtr<TSFTextStore> kungFuDeathGrip(this); + DispatchEvent(charAtPt); + if (!mWidget || mWidget->Destroyed()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "mWidget was destroyed during eQueryCharacterAtPoint", this)); + return E_FAIL; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::GetACPFromPoint(), charAtPt={ " + "mSucceeded=%s, mReply={ mOffset=%u, mTentativeCaretOffset=%u }}", + this, GetBoolName(charAtPt.mSucceeded), charAtPt.mReply.mOffset, + charAtPt.mReply.mTentativeCaretOffset)); + + if (NS_WARN_IF(!charAtPt.mSucceeded)) { + MOZ_LOG(sTextStoreLog, 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) && + charAtPt.mReply.mOffset == WidgetQueryContentEvent::NOT_FOUND) { + MOZ_LOG(sTextStoreLog, 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. + if (NS_WARN_IF(charAtPt.mReply.mTentativeCaretOffset == + WidgetQueryContentEvent::NOT_FOUND)) { + charAtPt.mReply.mTentativeCaretOffset = 0; + } + + 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 = charAtPt.mReply.mTentativeCaretOffset; + } else if (charAtPt.mReply.mOffset != WidgetQueryContentEvent::NOT_FOUND) { + // Otherwise, we should return character offset whose bounding box contains + // the point. + offset = charAtPt.mReply.mOffset; + } 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 = charAtPt.mReply.mTentativeCaretOffset; + + // However, if it's after the last character, we need to decrement the + // offset. + Content& contentForTSF = ContentForTSFRef(); + if (!contentForTSF.IsInitialized()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "ContentForTSFRef() failure", this)); + return E_FAIL; + } + if (contentForTSF.Text().Length() <= offset) { + // If the tentative caret is after the last character, let's return + // the last character's offset. + offset = contentForTSF.Text().Length() - 1; + } + } + + if (NS_WARN_IF(offset > LONG_MAX)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetACPFromPoint() succeeded: *pacp=%d", + this, *pacp)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetTextExt(TsViewCookie vcView, + LONG acpStart, + LONG acpEnd, + RECT* prc, + BOOL* pfClipped) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetTextExt(vcView=%ld, " + "acpStart=%ld, acpEnd=%ld, prc=0x%p, pfClipped=0x%p), " + "mDeferNotifyingTSF=%s, mWaitingQueryLayout=%s", + this, vcView, acpStart, acpEnd, prc, pfClipped, + GetBoolName(mDeferNotifyingTSF), GetBoolName(mWaitingQueryLayout))); + + if (!IsReadLocked()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "not locked (read)", this)); + return TS_E_NOLOCK; + } + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "called with invalid view", this)); + return E_INVALIDARG; + } + + if (!prc || !pfClipped) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + if (acpStart < 0 || acpEnd < acpStart) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "invalid position", this)); + return TS_E_INVALIDPOS; + } + + mWaitingQueryLayout = false; + + // NOTE: TSF (at least on Win 8.1) doesn't return TS_E_NOLAYOUT to the + // caller even if we return it. It's converted to just E_FAIL. + // However, this is fixed on Win 10. + + bool dontReturnNoLayoutError = false; + + const TSFStaticSink* kSink = TSFStaticSink::GetInstance(); + if (mComposition.IsComposing() && mComposition.mStart < acpEnd && + mContentForTSF.IsLayoutChangedAt(acpEnd)) { + const Selection& selectionForTSF = SelectionForTSFRef(); + // 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. + const bool kIsMSOfficeJapaneseIME2010 = + kSink->IsMSOfficeJapaneseIME2010Active(); + // 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. + if (kIsMSOfficeJapaneseIME2010 || + ((sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar || + sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret) && + kSink->IsMSJapaneseIMEActive())) { + // 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 ((kIsMSOfficeJapaneseIME2010 || + sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar) && + acpStart < acpEnd) { + acpEnd = acpStart; + dontReturnNoLayoutError = true; + } + // 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. + else if ((kIsMSOfficeJapaneseIME2010 || + sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret) && + acpStart == acpEnd && + selectionForTSF.IsCollapsed() && + selectionForTSF.EndOffset() == acpEnd) { + if (mContentForTSF.MinOffsetOfLayoutChanged() > LONG_MAX) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt(), FAILED due to the text " + "is too big for TSF (cannot treat modified offset as LONG), " + "mContentForTSF.MinOffsetOfLayoutChanged()=%u", + this, mContentForTSF.MinOffsetOfLayoutChanged())); + return E_FAIL; + } + int32_t minOffsetOfLayoutChanged = + static_cast<int32_t>(mContentForTSF.MinOffsetOfLayoutChanged()); + acpEnd = acpStart = std::max(minOffsetOfLayoutChanged - 1, 0); + dontReturnNoLayoutError = true; + } + } + // 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 + // composition string. + // XXX 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. + else if (sDoNotReturnNoLayoutErrorToATOKOfCompositionString && + kSink->IsATOKActive() && + (!kSink->IsATOKReferringNativeCaretActive() || + !sCreateNativeCaretForLegacyATOK) && + mComposition.mStart == acpStart && + mComposition.EndOffset() == acpEnd) { + dontReturnNoLayoutError = true; + } + // Free ChangJie 2010 and Easy Changjei 1.0.12.0 doesn't handle + // ITfContextView::GetTextExt() properly. Prehaps, it's due to the bug of + // TSF. We need to check if this is necessary on Windows 10 before + // disabling this on Windows 10. + else if ((sDoNotReturnNoLayoutErrorToFreeChangJie && + kSink->IsFreeChangJieActive()) || + (sDoNotReturnNoLayoutErrorToEasyChangjei && + kSink->IsEasyChangjeiActive())) { + acpEnd = mComposition.mStart; + acpStart = std::min(acpStart, acpEnd); + dontReturnNoLayoutError = true; + } + // Some Chinese TIPs of Microsoft doesn't show candidate window in e10s + // mode on Win8 or later. + else if (IsWin8OrLater() && + ((sDoNotReturnNoLayoutErrorToMSTraditionalTIP && + (kSink->IsMSChangJieActive() || + kSink->IsMSQuickQuickActive())) || + (sDoNotReturnNoLayoutErrorToMSSimplifiedTIP && + (kSink->IsMSPinyinActive() || + kSink->IsMSWubiActive())))) { + acpEnd = mComposition.mStart; + acpStart = std::min(acpStart, acpEnd); + dontReturnNoLayoutError = true; + } + + // 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. + if (dontReturnNoLayoutError) { + MOZ_ASSERT(mContentForTSF.IsLayoutChanged()); + if (mContentForTSF.MinOffsetOfLayoutChanged() > LONG_MAX) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt(), FAILED due to the text " + "is too big for TSF (cannot treat modified offset as LONG), " + "mContentForTSF.MinOffsetOfLayoutChanged()=%u", + this, mContentForTSF.MinOffsetOfLayoutChanged())); + return E_FAIL; + } + // 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.MinOffsetOfLayoutChanged()); + LONG lastUnmodifiedOffset = std::max(firstModifiedOffset - 1, 0); + if (mContentForTSF.IsLayoutChangedAt(acpStart)) { + // If TSF queries text rect in composition string, we should return + // rect at start of the composition even if its layout is changed. + if (acpStart >= mComposition.mStart) { + acpStart = mComposition.mStart; + } + // Otherwise, use first character's rect. Even if there is no + // characters, the query event will return caret rect instead. + else { + acpStart = lastUnmodifiedOffset; + } + MOZ_ASSERT(acpStart <= acpEnd); + } + if (mContentForTSF.IsLayoutChangedAt(acpEnd)) { + // Use max larger offset of last unmodified offset or acpStart which + // may be the first character offset of the composition string. + acpEnd = std::max(acpStart, lastUnmodifiedOffset); + } + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::GetTextExt() hacked the queried range " + "for not returning TS_E_NOLAYOUT, new values are: " + "acpStart=%d, acpEnd=%d", this, acpStart, acpEnd)); + } + } + + if (!dontReturnNoLayoutError && mContentForTSF.IsLayoutChangedAt(acpEnd)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT " + "(acpEnd=%d)", this, acpEnd)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + if (mDestroyed) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT " + "(acpEnd=%d) because this has already been destroyed", + this, acpEnd)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + // use eQueryTextRect to get rect in system, screen coordinates + WidgetQueryContentEvent event(true, eQueryTextRect, mWidget); + mWidget->InitEvent(event); + + WidgetQueryContentEvent::Options options; + int64_t startOffset = acpStart; + if (mComposition.IsComposing()) { + // 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.mStart; + } 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 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); + event.InitForQueryTextRect(startOffset, length, options); + + DispatchEvent(event); + if (NS_WARN_IF(!event.mSucceeded)) { + MOZ_LOG(sTextStoreLog, 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 (event.mReply.mRect.width <= 0) + event.mReply.mRect.width = 1; + if (event.mReply.mRect.height <= 0) + event.mReply.mRect.height = 1; + + // convert to unclipped screen rect + nsWindow* refWindow = static_cast<nsWindow*>( + event.mReply.mFocusedWidget ? event.mReply.mFocusedWidget : mWidget); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(false); + if (!refWindow) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "no top level window", this)); + return E_FAIL; + } + + event.mReply.mRect.MoveBy(refWindow->WidgetToScreenOffset()); + + // get bounding screen rect to test for clipping + if (!GetScreenExtInternal(*prc)) { + MOZ_LOG(sTextStoreLog, 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, event.mReply.mRect.x, event.mReply.mRect.y, + event.mReply.mRect.XMost(), event.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. Therefore, we need to create native caret only when ATOK 2011 - + // 2016 is active. + if (sCreateNativeCaretForLegacyATOK && + kSink->IsATOKReferringNativeCaretActive() && + mComposition.IsComposing() && + mComposition.mStart <= acpStart && mComposition.EndOffset() >= acpStart && + mComposition.mStart <= acpEnd && mComposition.EndOffset() >= acpEnd) { + CreateNativeCaret(); + } + + MOZ_LOG(sTextStoreLog, 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; +} + +STDMETHODIMP +TSFTextStore::GetScreenExt(TsViewCookie vcView, + RECT* prc) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::GetScreenExt(vcView=%ld, prc=0x%p)", + this, vcView, prc)); + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "called with invalid view", this)); + return E_INVALIDARG; + } + + if (!prc) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + if (mDestroyed) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() returns empty rect " + "due to already destroyed", this)); + prc->left = prc->top = prc->right = prc->left = 0; + return S_OK; + } + + if (!GetScreenExtInternal(*prc)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "GetScreenExtInternal() failure", this)); + return E_FAIL; + } + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::GetScreenExtInternal()", this)); + + MOZ_ASSERT(!mDestroyed); + + // use NS_QUERY_EDITOR_RECT to get rect in system, screen coordinates + WidgetQueryContentEvent event(true, eQueryEditorRect, mWidget); + mWidget->InitEvent(event); + DispatchEvent(event); + if (!event.mSucceeded) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExtInternal() FAILED due to " + "eQueryEditorRect failure", this)); + return false; + } + + nsWindow* refWindow = static_cast<nsWindow*>( + event.mReply.mFocusedWidget ? + event.mReply.mFocusedWidget : mWidget); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(false); + if (!refWindow) { + MOZ_LOG(sTextStoreLog, 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(event.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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetWnd() FAILED due to " + "called with invalid view", this)); + return E_INVALIDARG; + } + + if (!phwnd) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + *phwnd = mWidget ? mWidget->GetWindowHandle() : nullptr; + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::InsertTextAtSelection(dwFlags=%s, " + "pchText=0x%p \"%s\", cch=%lu, pacpStart=0x%p, pacpEnd=0x%p, " + "pChange=0x%p), IsComposing()=%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, + GetBoolName(mComposition.IsComposing()))); + + if (cch && !pchText) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null pchText", this)); + return E_INVALIDARG; + } + + if (TS_IAS_QUERYONLY == dwFlags) { + if (!IsReadLocked()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "not locked (read)", this)); + return TS_E_NOLOCK; + } + + if (!pacpStart || !pacpEnd) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + // Get selection first + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "SelectionForTSFRef() failure", this)); + return E_FAIL; + } + + // Simulate text insertion + *pacpStart = selectionForTSF.StartOffset(); + *pacpEnd = selectionForTSF.EndOffset(); + if (pChange) { + pChange->acpStart = selectionForTSF.StartOffset(); + pChange->acpOldEnd = selectionForTSF.EndOffset(); + pChange->acpNewEnd = + selectionForTSF.StartOffset() + static_cast<LONG>(cch); + } + } else { + if (!IsReadWriteLocked()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "not locked (read-write)", this)); + return TS_E_NOLOCK; + } + + if (!pChange) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null pChange", this)); + return E_INVALIDARG; + } + + if (TS_IAS_NOQUERY != dwFlags && (!pacpStart || !pacpEnd)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null argument", this)); + return E_INVALIDARG; + } + + if (!InsertTextAtSelectionInternal(nsDependentSubstring(pchText, cch), + pChange)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal(" + "aInsertStr=\"%s\", aTextChange=0x%p), IsComposing=%s", + this, GetEscapedUTF8String(aInsertStr).get(), aTextChange, + GetBoolName(mComposition.IsComposing()))); + + Content& contentForTSF = ContentForTSFRef(); + if (!contentForTSF.IsInitialized()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() failed " + "due to ContentForTSFRef() failure()", this)); + return false; + } + + TS_SELECTION_ACP oldSelection = contentForTSF.Selection().ACP(); + if (!mComposition.IsComposing()) { + // Use a temporary composition to contain the text + PendingAction* compositionStart = mPendingActions.AppendElement(); + compositionStart->mType = PendingAction::COMPOSITION_START; + compositionStart->mSelectionStart = oldSelection.acpStart; + compositionStart->mSelectionLength = + oldSelection.acpEnd - oldSelection.acpStart; + compositionStart->mAdjustSelection = false; + + PendingAction* compositionEnd = mPendingActions.AppendElement(); + compositionEnd->mType = PendingAction::COMPOSITION_END; + compositionEnd->mData = aInsertStr; + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() " + "appending pending compositionstart and compositionend... " + "PendingCompositionStart={ mSelectionStart=%d, " + "mSelectionLength=%d }, PendingCompositionEnd={ mData=\"%s\" " + "(Length()=%u) }", + this, compositionStart->mSelectionStart, + compositionStart->mSelectionLength, + GetEscapedUTF8String(compositionEnd->mData).get(), + compositionEnd->mData.Length())); + } + + contentForTSF.ReplaceSelectedTextWith(aInsertStr); + + if (aTextChange) { + aTextChange->acpStart = oldSelection.acpStart; + aTextChange->acpOldEnd = oldSelection.acpEnd; + aTextChange->acpNewEnd = contentForTSF.Selection().EndOffset(); + } + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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* aComposition, + ITfRange* aRange, + bool aPreserveSelection) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionStartAction(" + "aComposition=0x%p, aRange=0x%p, aPreserveSelection=%s), " + "mComposition.mView=0x%p", + this, aComposition, aRange, GetBoolName(aPreserveSelection), + mComposition.mView.get())); + + LONG start = 0, length = 0; + HRESULT hr = GetRangeExtent(aRange, &start, &length); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to GetRangeExtent() failure", this)); + return hr; + } + + return RecordCompositionStartAction(aComposition, start, length, + aPreserveSelection); +} + +HRESULT +TSFTextStore::RecordCompositionStartAction(ITfCompositionView* aComposition, + LONG aStart, + LONG aLength, + bool aPreserveSelection) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionStartAction(" + "aComposition=0x%p, aStart=%d, aLength=%d, aPreserveSelection=%s), " + "mComposition.mView=0x%p", + this, aComposition, aStart, aLength, GetBoolName(aPreserveSelection), + mComposition.mView.get())); + + Content& contentForTSF = ContentForTSFRef(); + if (!contentForTSF.IsInitialized()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to ContentForTSFRef() failure", this)); + return E_FAIL; + } + + CompleteLastActionIfStillIncomplete(); + + // TIP may have inserted text at selection before calling + // OnStartComposition(). In this case, we've already created a pair of + // pending compositionstart and pending compositionend. If the pending + // compositionstart occurred same range as this composition, it was the + // start of this composition. In such case, we should cancel the pending + // compositionend and start composition normally. + if (!aPreserveSelection && + WasTextInsertedWithoutCompositionAt(aStart, aLength)) { + const PendingAction& pendingCompositionEnd = mPendingActions.LastElement(); + const PendingAction& pendingCompositionStart = + mPendingActions[mPendingActions.Length() - 2]; + contentForTSF.RestoreCommittedComposition( + aComposition, pendingCompositionStart, pendingCompositionEnd); + mPendingActions.RemoveElementAt(mPendingActions.Length() - 1); + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionStartAction() " + "succeeded: restoring the committed string as composing string, " + "mComposition={ mStart=%ld, mString.Length()=%ld, " + "mSelectionForTSF={ acpStart=%ld, acpEnd=%ld, style.ase=%s, " + "style.fInterimChar=%s } }", + this, mComposition.mStart, mComposition.mString.Length(), + mSelectionForTSF.StartOffset(), mSelectionForTSF.EndOffset(), + GetActiveSelEndName(mSelectionForTSF.ActiveSelEnd()), + GetBoolName(mSelectionForTSF.IsInterimChar()))); + return S_OK; + } + + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::COMPOSITION_START; + action->mSelectionStart = aStart; + action->mSelectionLength = aLength; + + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to SelectionForTSFRef() failure", this)); + 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(aComposition, *action, aPreserveSelection); + action->mData = mComposition.mString; + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionStartAction() succeeded: " + "mComposition={ mStart=%ld, mString.Length()=%ld, " + "mSelectionForTSF={ acpStart=%ld, acpEnd=%ld, style.ase=%s, " + "style.fInterimChar=%s } }", + this, mComposition.mStart, mComposition.mString.Length(), + mSelectionForTSF.StartOffset(), mSelectionForTSF.EndOffset(), + GetActiveSelEndName(mSelectionForTSF.ActiveSelEnd()), + GetBoolName(mSelectionForTSF.IsInterimChar()))); + return S_OK; +} + +HRESULT +TSFTextStore::RecordCompositionEndAction() +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionEndAction(), " + "mComposition={ mView=0x%p, mString=\"%s\" }", + this, mComposition.mView.get(), + GetEscapedUTF8String(mComposition.mString).get())); + + MOZ_ASSERT(mComposition.IsComposing()); + + CompleteLastActionIfStillIncomplete(); + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::COMPOSITION_END; + action->mData = mComposition.mString; + + Content& contentForTSF = ContentForTSFRef(); + if (!contentForTSF.IsInitialized()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due " + "to ContentForTSFRef() 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::COMPOSITION_START) { + if (pendingAction.mData != action->mData) { + break; + } + // When only setting selection is necessary, we should append it. + if (pendingAction.mAdjustSelection) { + PendingAction* setSelection = mPendingActions.AppendElement(); + setSelection->mType = PendingAction::SET_SELECTION; + setSelection->mSelectionStart = pendingAction.mSelectionStart; + setSelection->mSelectionLength = pendingAction.mSelectionLength; + setSelection->mSelectionReversed = false; + } + // Remove the redundant pending composition. + mPendingActions.RemoveElementsAt(i - 1, j); + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionEndAction(), " + "succeeded, but the composition was canceled due to redundant", + this)); + return S_OK; + } + } + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionEndAction(), succeeded", + this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnStartComposition(ITfCompositionView* pComposition, + BOOL* pfOk) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnStartComposition(pComposition=0x%p, " + "pfOk=0x%p), mComposition.mView=0x%p", + this, pComposition, pfOk, mComposition.mView.get())); + + AutoPendingActionAndContentFlusher flusher(this); + + *pfOk = FALSE; + + // Only one composition at a time + if (mComposition.IsComposing()) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnStartComposition() FAILED due to " + "RecordCompositionStartAction() failure", this)); + return hr; + } + + *pfOk = TRUE; + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnStartComposition() succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnUpdateComposition(ITfCompositionView* pComposition, + ITfRange* pRangeNew) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition(pComposition=0x%p, " + "pRangeNew=0x%p), mComposition.mView=0x%p", + this, pComposition, pRangeNew, mComposition.mView.get())); + + AutoPendingActionAndContentFlusher flusher(this); + + if (!mDocumentMgr || !mContext) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "not ready for the composition", this)); + return E_UNEXPECTED; + } + if (!mComposition.IsComposing()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "no active composition", this)); + return E_UNEXPECTED; + } + if (mComposition.mView != pComposition) { + MOZ_LOG(sTextStoreLog, 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) { + PendingAction* action = LastOrNewPendingCompositionUpdate(); + action->mIncomplete = true; + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition() succeeded but " + "not complete", this)); + return S_OK; + } + + HRESULT hr = RestartCompositionIfNecessary(pRangeNew); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "RestartCompositionIfNecessary() failure", this)); + return hr; + } + + hr = RecordCompositionUpdateAction(); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "RecordCompositionUpdateAction() failure", this)); + return hr; + } + + if (MOZ_LOG_TEST(sTextStoreLog, LogLevel::Info)) { + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "SelectionForTSFRef() failure", this)); + return E_FAIL; + } + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition() succeeded: " + "mComposition={ mStart=%ld, mString=\"%s\" }, " + "SelectionForTSFRef()={ acpStart=%ld, acpEnd=%ld, style.ase=%s }", + this, mComposition.mStart, + GetEscapedUTF8String(mComposition.mString).get(), + selectionForTSF.StartOffset(), selectionForTSF.EndOffset(), + GetActiveSelEndName(selectionForTSF.ActiveSelEnd()))); + } + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnEndComposition(ITfCompositionView* pComposition) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnEndComposition(pComposition=0x%p), " + "mComposition={ mView=0x%p, mString=\"%s\" }", + this, pComposition, mComposition.mView.get(), + GetEscapedUTF8String(mComposition.mString).get())); + + AutoPendingActionAndContentFlusher flusher(this); + + if (!mComposition.IsComposing()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "no active composition", this)); + return E_UNEXPECTED; + } + + if (mComposition.mView != pComposition) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "RecordCompositionEndAction() failure", this)); + return hr; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnEndComposition(), succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::AdviseMouseSink(ITfRangeACP* range, + ITfMouseSink* pSink, + DWORD* pdwCookie) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseMouseSink(range=0x%p, pSink=0x%p, " + "pdwCookie=0x%p)", this, range, pSink, pdwCookie)); + + if (!pdwCookie) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the " + "range is null", this)); + return E_INVALIDARG; + } + if (!pSink) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to failure " + "of MouseTracker::Init()", this)); + return hr; + } + *pdwCookie = tracker->Cookie(); + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseMouseSink(), succeeded, " + "*pdwCookie=%d", this, *pdwCookie)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::UnadviseMouseSink(DWORD dwCookie) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseMouseSink(dwCookie=%d)", + this, dwCookie)); + if (dwCookie == MouseTracker::kInvalidCookie) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to " + "the found tracker uninstalled already", this)); + return E_INVALIDARG; + } + tracker.UnadviseSink(); + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseMouseSink(), succeeded", this)); + return S_OK; +} + +// static +nsresult +TSFTextStore::OnFocusChange(bool aGotFocus, + nsWindowBase* aFocusedWidget, + const InputContext& aContext) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + (" TSFTextStore::OnFocusChange(aGotFocus=%s, " + "aFocusedWidget=0x%p, aContext={ mIMEState={ mEnabled=%s }, " + "mHTMLInputType=\"%s\" }), " + "sThreadMgr=0x%p, sEnabledTextStore=0x%p", + GetBoolName(aGotFocus), aFocusedWidget, + GetIMEEnabledName(aContext.mIMEState.mEnabled), + NS_ConvertUTF16toUTF8(aContext.mHTMLInputType).get(), + sThreadMgr.get(), sEnabledTextStore.get())); + + if (NS_WARN_IF(!IsInTSFMode())) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<ITfDocumentMgr> prevFocusedDocumentMgr; + RefPtr<TSFTextStore> oldTextStore = sEnabledTextStore.forget(); + + // If currently sEnableTextStore has focus, notifies TSF of losing focus. + if (ThinksHavingFocus()) { + 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"); + } + + // If there is sEnabledTextStore, we don't use it in the new focused editor. + // Release it now. + if (oldTextStore) { + oldTextStore->Destroy(); + } + + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(nsWindowBase* 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "invalid TSFTextStore::mDocumentMgr")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (aContext.mIMEState.mEnabled == IMEState::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; + { + // Windows 10's softwware keyboard requires that SetSelection must be + // always successful into SetFocus. If returning error, it might crash + // into TextInputFramework.dll. + AutoSetTemporarySelection setSelection(textStore->SelectionForTSFRef()); + + hr = threadMgr->SetFocus(newDocMgr); + } + + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "ITfTheadMgr::SetFocus() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "ITfTheadMgr::AssociateFocus() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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 +nsIMEUpdatePreference +TSFTextStore::GetIMEUpdatePreference() +{ + if (sThreadMgr && sEnabledTextStore && sEnabledTextStore->mDocumentMgr) { + RefPtr<ITfDocumentMgr> docMgr; + sThreadMgr->GetFocus(getter_AddRefs(docMgr)); + if (docMgr == sEnabledTextStore->mDocumentMgr) { + return nsIMEUpdatePreference( + nsIMEUpdatePreference::NOTIFY_TEXT_CHANGE | + nsIMEUpdatePreference::NOTIFY_POSITION_CHANGE | + nsIMEUpdatePreference::NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR | + nsIMEUpdatePreference::NOTIFY_DURING_DEACTIVE); + } + } + return nsIMEUpdatePreference(); +} + +nsresult +TSFTextStore::OnTextChangeInternal(const IMENotification& aIMENotification) +{ + const TextChangeDataBase& textChangeData = aIMENotification.mTextChangeData; + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::OnTextChangeInternal(aIMENotification={ " + "mMessage=0x%08X, mTextChangeData={ mStartOffset=%lu, " + "mRemovedEndOffset=%lu, mAddedEndOffset=%lu, " + "mCausedOnlyByComposition=%s, " + "mIncludingChangesDuringComposition=%s, " + "mIncludingChangesWithoutComposition=%s }), " + "mDestroyed=%s, mSink=0x%p, mSinkMask=%s, " + "mComposition.IsComposing()=%s", + this, aIMENotification.mMessage, + textChangeData.mStartOffset, + textChangeData.mRemovedEndOffset, + textChangeData.mAddedEndOffset, + GetBoolName(textChangeData.mCausedOnlyByComposition), + GetBoolName(textChangeData.mIncludingChangesDuringComposition), + GetBoolName(textChangeData.mIncludingChangesWithoutComposition), + GetBoolName(mDestroyed), + mSink.get(), + GetSinkMaskNameStr(mSinkMask).get(), + GetBoolName(mComposition.IsComposing()))); + + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + mDeferNotifyingTSF = 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.IsComposing()); + 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.MarkDirty(); + + // 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::OnSelectionChangeInternal(" + "aIMENotification={ mSelectionChangeData={ mOffset=%lu, " + "Length()=%lu, mReversed=%s, mWritingMode=%s, " + "mCausedByComposition=%s, mCausedBySelectionEvent=%s, " + "mOccurredDuringComposition=%s } }), mDestroyed=%s, " + "mSink=0x%p, mSinkMask=%s, mIsRecordingActionsWithoutLock=%s, " + "mComposition.IsComposing()=%s", + this, selectionChangeData.mOffset, selectionChangeData.Length(), + GetBoolName(selectionChangeData.mReversed), + GetWritingModeName(selectionChangeData.GetWritingMode()).get(), + GetBoolName(selectionChangeData.mCausedByComposition), + GetBoolName(selectionChangeData.mCausedBySelectionEvent), + GetBoolName(selectionChangeData.mOccurredDuringComposition), + GetBoolName(mDestroyed), + mSink.get(), GetSinkMaskNameStr(mSinkMask).get(), + GetBoolName(mIsRecordingActionsWithoutLock), + GetBoolName(mComposition.IsComposing()))); + + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + mDeferNotifyingTSF = 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.Assign(selectionChangeData); + + // Flush remaining pending notifications here if it's possible. + MaybeFlushPendingNotifications(); + + return NS_OK; +} + +void +TSFTextStore::NotifyTSFOfSelectionChange() +{ + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(!IsReadLocked()); + MOZ_ASSERT(!mComposition.IsComposing()); + MOZ_ASSERT(mPendingSelectionChangeData.IsValid()); + + // If selection range isn't actually changed, we don't need to notify TSF + // of this selection change. + if (!mSelectionForTSF.SetSelection( + mPendingSelectionChangeData.mOffset, + mPendingSelectionChangeData.Length(), + mPendingSelectionChangeData.mReversed, + mPendingSelectionChangeData.GetWritingMode())) { + mPendingSelectionChangeData.Clear(); + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::NotifyTSFOfSelectionChange(), " + "selection isn't actually changed.", this)); + return; + } + + mPendingSelectionChangeData.Clear(); + + if (!mSink || !(mSinkMask & TS_AS_SEL_CHANGE)) { + return; + } + + MOZ_LOG(sTextStoreLog, 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); + + mDeferNotifyingTSF = 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::OnLayoutChangeInternal(), calling " + "NotifyTSFOfLayoutChange()...", this)); + if (NS_WARN_IF(!NotifyTSFOfLayoutChange())) { + rv = NS_ERROR_FAILURE; + } + + MOZ_LOG(sTextStoreLog, 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.IsInitialized()) { + mContentForTSF.OnLayoutChanged(); + } + + // Now, the caret position is different from ours. Destroy the native caret + // if there is. + MaybeDestroyNativeCaret(); + + // This method should return true if either way succeeds. + bool ret = true; + + if (mSink) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "calling ITfContextOwnerServices::OnLayoutChange()...", + this)); + HRESULT hr = service->OnLayoutChange(); + ret = ret && SUCCEEDED(hr); + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "called ITfContextOwnerServices::OnLayoutChange()", + this)); + } + } + + if (!mWidget || mWidget->Destroyed()) { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "the widget is destroyed during calling OnLayoutChange()", + this)); + return ret; + } + + if (mDestroyed) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Warning, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "called NotifyTSFOfLayoutChange() but TIP didn't retry to " + "retrieve the layout information", this)); + } else { + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "called NotifyTSFOfLayoutChange()", this)); + } +} + +nsresult +TSFTextStore::OnUpdateCompositionInternal() +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::OnUpdateCompositionInternal(), " + "mDestroyed=%s, mDeferNotifyingTSF=%s", + this, GetBoolName(mDestroyed), GetBoolName(mDeferNotifyingTSF))); + + // There are nothing to do after destroyed. + if (mDestroyed) { + return NS_OK; + } + + // 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 until + // starting next composition. + if (!mComposition.IsComposing() && !IsComposingInContent()) { + mDeferClearingContentForTSF = false; + } + mDeferNotifyingTSF = false; + MaybeFlushPendingNotifications(); + 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::OnMouseButtonEventInternal(" + "aIMENotification={ mEventMessage=%s, mOffset=%u, mCursorPos={ " + "mX=%d, mY=%d }, mCharRect={ mX=%d, mY=%d, mWidth=%d, mHeight=%d }, " + "mButton=%s, mButtons=%s, mModifiers=%s })", + this, ToChar(aIMENotification.mMouseButtonEventData.mEventMessage), + aIMENotification.mMouseButtonEventData.mOffset, + aIMENotification.mMouseButtonEventData.mCursorPos.mX, + aIMENotification.mMouseButtonEventData.mCursorPos.mY, + aIMENotification.mMouseButtonEventData.mCharRect.mX, + aIMENotification.mMouseButtonEventData.mCharRect.mY, + aIMENotification.mMouseButtonEventData.mCharRect.mWidth, + aIMENotification.mMouseButtonEventData.mCharRect.mHeight, + GetMouseButtonName(aIMENotification.mMouseButtonEventData.mButton), + GetMouseButtonsName( + aIMENotification.mMouseButtonEventData.mButtons).get(), + GetModifiersName( + aIMENotification.mMouseButtonEventData.mModifiers).get())); + + uint32_t offset = aIMENotification.mMouseButtonEventData.mOffset; + nsIntRect charRect = + aIMENotification.mMouseButtonEventData.mCharRect.AsIntRect(); + nsIntPoint cursorPos = + aIMENotification.mMouseButtonEventData.mCursorPos.AsIntPoint(); + 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 WidgetMouseEventBase::eLeftButton: + buttonStatus = MK_LBUTTON; + break; + case WidgetMouseEventBase::eMiddleButton: + buttonStatus = MK_MBUTTON; + break; + case WidgetMouseEventBase::eRightButton: + 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.InRange(offset)) { + continue; + } + if (tracker.OnMouseButtonEvent(edge - tracker.RangeStart(), + quadrant, buttonStatus)) { + return NS_SUCCESS_EVENT_CONSUMED; + } + } + return NS_OK; +} + +void +TSFTextStore::CreateNativeCaret() +{ + MaybeDestroyNativeCaret(); + + // Don't create native caret after destroyed. + if (mDestroyed) { + return; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::CreateNativeCaret(), " + "mComposition.IsComposing()=%s", + this, GetBoolName(mComposition.IsComposing()))); + + Selection& selectionForTSF = SelectionForTSFRef(); + if (selectionForTSF.IsDirty()) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "SelectionForTSFRef() failure", this)); + return; + } + + WidgetQueryContentEvent queryCaretRect(true, eQueryCaretRect, mWidget); + mWidget->InitEvent(queryCaretRect); + + WidgetQueryContentEvent::Options options; + // XXX If this is called without composition and the selection isn't + // collapsed, is it OK? + int64_t caretOffset = selectionForTSF.MaxOffset(); + if (mComposition.IsComposing()) { + // If there is a composition, use insertion point relative query for + // deciding caret position because composition might be at different + // position where TSFTextStore believes it at. + options.mRelativeToInsertionPoint = true; + caretOffset -= mComposition.mStart; + } 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 relative to insertion point query since + // TSF/TIP computes the offset from the cached selection. + options.mRelativeToInsertionPoint = true; + caretOffset -= mSelectionForTSF.StartOffset(); + } + queryCaretRect.InitForQueryCaretRect(caretOffset, options); + + DispatchEvent(queryCaretRect); + if (NS_WARN_IF(!queryCaretRect.mSucceeded)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "eQueryCaretRect failure (offset=%d)", this, caretOffset)); + return; + } + + LayoutDeviceIntRect& caretRect = queryCaretRect.mReply.mRect; + mNativeCaretIsCreated = ::CreateCaret(mWidget->GetWindowHandle(), nullptr, + caretRect.width, caretRect.height); + if (!mNativeCaretIsCreated) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "CreateCaret() failure", this)); + return; + } + + nsWindow* window = static_cast<nsWindow*>(mWidget.get()); + nsWindow* toplevelWindow = window->GetTopLevelWindow(false); + if (!toplevelWindow) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "no top level window", this)); + return; + } + + if (toplevelWindow != window) { + caretRect.MoveBy(toplevelWindow->WidgetToScreenOffset()); + caretRect.MoveBy(-window->WidgetToScreenOffset()); + } + + ::SetCaretPos(caretRect.x, caretRect.y); +} + +void +TSFTextStore::MaybeDestroyNativeCaret() +{ + if (!mNativeCaretIsCreated) { + return; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDestroyNativeCaret(), " + "destroying native caret", this)); + + ::DestroyCaret(); + mNativeCaretIsCreated = false; +} + +void +TSFTextStore::CommitCompositionInternal(bool aDiscard) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(aDiscard=%s), " + "mSink=0x%p, mContext=0x%p, mComposition.mView=0x%p, " + "mComposition.mString=\"%s\"", + this, GetBoolName(aDiscard), mSink.get(), mContext.get(), + mComposition.mView.get(), + GetEscapedUTF8String(mComposition.mString).get())); + + // 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(), " + "putting off to request to %s composition after unlocking the document", + this, aDiscard ? "cancel" : "commit")); + return; + } + + if (mComposition.IsComposing() && aDiscard) { + LONG endOffset = mComposition.EndOffset(); + mComposition.mString.Truncate(0); + // Note that don't notify TSF of text change after this is destroyed. + if (mSink && !mDestroyed) { + TS_TEXTCHANGE textChange; + textChange.acpStart = mComposition.mStart; + textChange.acpOldEnd = endOffset; + textChange.acpNewEnd = mComposition.mStart; + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("TSFTextStore::SetIMEOpenState(aState=%s)", + GetBoolName(aState))); + + RefPtr<ITfCompartment> comp; + if (!GetCompartment(sThreadMgr, + GUID_COMPARTMENT_KEYBOARD_OPENCLOSE, + getter_AddRefs(comp))) { + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + (" TSFTextStore::SetIMEOpenState() FAILED due to" + "no compartment available")); + return; + } + + VARIANT variant; + variant.vt = VT_I4; + variant.lVal = aState; + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + (" TSFTextStore::SetIMEOpenState(), setting " + "0x%04X to GUID_COMPARTMENT_KEYBOARD_OPENCLOSE...", + variant.lVal)); + comp->SetValue(sClientId, &variant); +} + +// static +bool +TSFTextStore::GetIMEOpenState() +{ + RefPtr<ITfCompartment> comp; + if (!GetCompartment(sThreadMgr, + GUID_COMPARTMENT_KEYBOARD_OPENCLOSE, + getter_AddRefs(comp))) + return false; + + VARIANT variant; + ::VariantInit(&variant); + if (SUCCEEDED(comp->GetValue(&variant)) && variant.vt == VT_I4) + return variant.lVal != 0; + + ::VariantClear(&variant); // clear up in case variant.vt != VT_I4 + return false; +} + +// static +void +TSFTextStore::SetInputContext(nsWindowBase* aWidget, + const InputContext& aContext, + const InputContextAction& aAction) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("TSFTextStore::SetInputContext(aWidget=%p, " + "aContext.mIMEState.mEnabled=%s, aAction.mFocusChange=%s), " + "sEnabledTextStore=0x%p, ThinksHavingFocus()=%s", + aWidget, GetIMEEnabledName(aContext.mIMEState.mEnabled), + GetFocusChangeName(aAction.mFocusChange), sEnabledTextStore.get(), + GetBoolName(ThinksHavingFocus()))); + + NS_ENSURE_TRUE_VOID(IsInTSFMode()); + + if (aAction.mFocusChange != InputContextAction::FOCUS_NOT_CHANGED) { + if (sEnabledTextStore) { + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + textStore->SetInputScope(aContext.mHTMLInputType, + aContext.mHTMLInputInputmode); + } + return; + } + + // If focus isn't actually changed but the enabled state is changed, + // emulate the focus move. + if (!ThinksHavingFocus() && aContext.mIMEState.IsEditable()) { + OnFocusChange(true, aWidget, aContext); + } else if (ThinksHavingFocus() && !aContext.mIMEState.IsEditable()) { + 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(sTextStoreLog, LogLevel::Error, + ("TSFTextStore::MarkContextAsKeyboardDisabled() failed" + "aContext=0x%p...", aContext)); + return; + } + + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + ("TSFTextStore::MarkContextAsEmpty() failed" + "aContext=0x%p...", aContext)); + return; + } + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("TSFTextStore::MarkContextAsEmpty(), setting " + "to mark empty context 0x%p...", aContext)); + comp->SetValue(sClientId, &variant_int4_value1); +} + +// static +void +TSFTextStore::Initialize() +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, + ("TSFTextStore::Initialize() is called...")); + + if (sThreadMgr) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED due to already initialized")); + return; + } + + bool enableTsf = + IsVistaOrLater() && Preferences::GetBool(kPrefNameEnableTSF, false); + MOZ_LOG(sTextStoreLog, LogLevel::Info, + (" TSFTextStore::Initialize(), TSF is %s", + enableTsf ? "enabled" : "disabled")); + if (!enableTsf) { + return; + } + + // XXX MSDN documents that ITfInputProcessorProfiles is available only on + // desktop apps. However, there is no known way to obtain + // ITfInputProcessorProfileMgr instance without ITfInputProcessorProfiles + // instance. + RefPtr<ITfInputProcessorProfiles> inputProcessorProfiles; + HRESULT hr = + ::CoCreateInstance(CLSID_TF_InputProcessorProfiles, nullptr, + CLSCTX_INPROC_SERVER, + IID_ITfInputProcessorProfiles, + getter_AddRefs(inputProcessorProfiles)); + if (FAILED(hr) || !inputProcessorProfiles) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create input processor " + "profiles, hr=0x%08X", hr)); + return; + } + + RefPtr<ITfThreadMgr> threadMgr; + hr = ::CoCreateInstance(CLSID_TF_ThreadMgr, nullptr, + CLSCTX_INPROC_SERVER, IID_ITfThreadMgr, + getter_AddRefs(threadMgr)); + if (FAILED(hr) || !threadMgr) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to " + "create the thread manager, hr=0x%08X", hr)); + return; + } + + RefPtr<ITfMessagePump> messagePump; + hr = threadMgr->QueryInterface(IID_ITfMessagePump, + getter_AddRefs(messagePump)); + if (FAILED(hr) || !messagePump) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to " + "QI message pump from the thread manager, hr=0x%08X", hr)); + return; + } + + RefPtr<ITfKeystrokeMgr> keystrokeMgr; + hr = threadMgr->QueryInterface(IID_ITfKeystrokeMgr, + getter_AddRefs(keystrokeMgr)); + if (FAILED(hr) || !keystrokeMgr) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to " + "QI keystroke manager from the thread manager, hr=0x%08X", hr)); + return; + } + + hr = threadMgr->Activate(&sClientId); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to activate, hr=0x%08X", hr)); + return; + } + + RefPtr<ITfDisplayAttributeMgr> displayAttributeMgr; + hr = ::CoCreateInstance(CLSID_TF_DisplayAttributeMgr, nullptr, + CLSCTX_INPROC_SERVER, IID_ITfDisplayAttributeMgr, + getter_AddRefs(displayAttributeMgr)); + if (FAILED(hr) || !displayAttributeMgr) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a display attribute manager instance, hr=0x%08X", hr)); + return; + } + + RefPtr<ITfCategoryMgr> categoryMgr; + hr = ::CoCreateInstance(CLSID_TF_CategoryMgr, nullptr, + CLSCTX_INPROC_SERVER, IID_ITfCategoryMgr, + getter_AddRefs(categoryMgr)); + if (FAILED(hr) || !categoryMgr) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a category manager instance, hr=0x%08X", hr)); + return; + } + + RefPtr<ITfDocumentMgr> disabledDocumentMgr; + hr = threadMgr->CreateDocumentMgr(getter_AddRefs(disabledDocumentMgr)); + if (FAILED(hr) || !disabledDocumentMgr) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a document manager for disabled mode, hr=0x%08X", hr)); + return; + } + + RefPtr<ITfContext> disabledContext; + DWORD editCookie = 0; + hr = disabledDocumentMgr->CreateContext(sClientId, 0, nullptr, + getter_AddRefs(disabledContext), + &editCookie); + if (FAILED(hr) || !disabledContext) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a context for disabled mode, hr=0x%08X", hr)); + return; + } + + MarkContextAsKeyboardDisabled(disabledContext); + MarkContextAsEmpty(disabledContext); + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + (" TSFTextStore::Initialize() is creating " + "a TSFStaticSink instance...")); + TSFStaticSink* staticSink = TSFStaticSink::GetInstance(); + if (!staticSink->Init(threadMgr, inputProcessorProfiles)) { + TSFStaticSink::Shutdown(); + MOZ_LOG(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to initialize TSFStaticSink " + "instance")); + return; + } + + sInputProcessorProfiles = inputProcessorProfiles; + sThreadMgr = threadMgr; + sMessagePump = messagePump; + sKeystrokeMgr = keystrokeMgr; + sDisplayAttrMgr = displayAttributeMgr; + sCategoryMgr = categoryMgr; + sDisabledDocumentMgr = disabledDocumentMgr; + sDisabledContext = disabledContext; + + sCreateNativeCaretForLegacyATOK = + Preferences::GetBool("intl.tsf.hack.atok.create_native_caret", true); + sDoNotReturnNoLayoutErrorToATOKOfCompositionString = + Preferences::GetBool( + "intl.tsf.hack.atok.do_not_return_no_layout_error_of_composition_string", + true); + sDoNotReturnNoLayoutErrorToMSSimplifiedTIP = + Preferences::GetBool( + "intl.tsf.hack.ms_simplified_chinese.do_not_return_no_layout_error", + true); + sDoNotReturnNoLayoutErrorToMSTraditionalTIP = + Preferences::GetBool( + "intl.tsf.hack.ms_traditional_chinese.do_not_return_no_layout_error", + true); + sDoNotReturnNoLayoutErrorToFreeChangJie = + Preferences::GetBool( + "intl.tsf.hack.free_chang_jie.do_not_return_no_layout_error", true); + sDoNotReturnNoLayoutErrorToEasyChangjei = + Preferences::GetBool( + "intl.tsf.hack.easy_changjei.do_not_return_no_layout_error", true); + sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar = + Preferences::GetBool( + "intl.tsf.hack.ms_japanese_ime." + "do_not_return_no_layout_error_at_first_char", true); + sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret = + Preferences::GetBool( + "intl.tsf.hack.ms_japanese_ime.do_not_return_no_layout_error_at_caret", + true); + sHackQueryInsertForMSSimplifiedTIP = + Preferences::GetBool( + "intl.tsf.hack.ms_simplified_chinese.query_insert_result", true); + sHackQueryInsertForMSTraditionalTIP = + Preferences::GetBool( + "intl.tsf.hack.ms_traditional_chinese.query_insert_result", true); + + MOZ_LOG(sTextStoreLog, LogLevel::Info, + (" TSFTextStore::Initialize(), sThreadMgr=0x%p, " + "sClientId=0x%08X, sDisplayAttrMgr=0x%p, " + "sCategoryMgr=0x%p, sDisabledDocumentMgr=0x%p, sDisabledContext=%p, " + "sCreateNativeCaretForLegacyATOK=%s, " + "sDoNotReturnNoLayoutErrorToATOKOfCompositionString=%s, " + "sDoNotReturnNoLayoutErrorToFreeChangJie=%s, " + "sDoNotReturnNoLayoutErrorToEasyChangjei=%s, " + "sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar=%s, " + "sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret=%s", + sThreadMgr.get(), sClientId, sDisplayAttrMgr.get(), + sCategoryMgr.get(), sDisabledDocumentMgr.get(), sDisabledContext.get(), + GetBoolName(sCreateNativeCaretForLegacyATOK), + GetBoolName(sDoNotReturnNoLayoutErrorToATOKOfCompositionString), + GetBoolName(sDoNotReturnNoLayoutErrorToFreeChangJie), + GetBoolName(sDoNotReturnNoLayoutErrorToEasyChangjei), + GetBoolName(sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar), + GetBoolName(sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret))); +} + +// static +void +TSFTextStore::Terminate() +{ + MOZ_LOG(sTextStoreLog, LogLevel::Info, ("TSFTextStore::Terminate()")); + + TSFStaticSink::Shutdown(); + + sDisplayAttrMgr = nullptr; + sCategoryMgr = nullptr; + sEnabledTextStore = nullptr; + sDisabledDocumentMgr = nullptr; + sDisabledContext = nullptr; + sInputProcessorProfiles = nullptr; + sClientId = 0; + if (sThreadMgr) { + sThreadMgr->Deactivate(); + sThreadMgr = nullptr; + sMessagePump = nullptr; + sKeystrokeMgr = nullptr; + } +} + +// static +bool +TSFTextStore::ProcessRawKeyMessage(const MSG& aMsg) +{ + if (!sKeystrokeMgr) { + return false; // not in TSF mode + } + + if (aMsg.message == WM_KEYDOWN) { + BOOL eaten; + RefPtr<ITfKeystrokeMgr> keystrokeMgr = sKeystrokeMgr; + HRESULT hr = keystrokeMgr->TestKeyDown(aMsg.wParam, aMsg.lParam, &eaten); + if (FAILED(hr) || !sKeystrokeMgr || !eaten) { + return false; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + if (textStore) { + textStore->OnStartToHandleKeyMessage(); + } + hr = keystrokeMgr->KeyDown(aMsg.wParam, aMsg.lParam, &eaten); + if (textStore) { + textStore->OnEndHandlingKeyMessage(); + } + return SUCCEEDED(hr) && (eaten || !sKeystrokeMgr); + } + if (aMsg.message == WM_KEYUP) { + BOOL eaten; + RefPtr<ITfKeystrokeMgr> keystrokeMgr = sKeystrokeMgr; + HRESULT hr = keystrokeMgr->TestKeyUp(aMsg.wParam, aMsg.lParam, &eaten); + if (FAILED(hr) || !sKeystrokeMgr || !eaten) { + return false; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + if (textStore) { + textStore->OnStartToHandleKeyMessage(); + } + hr = keystrokeMgr->KeyUp(aMsg.wParam, aMsg.lParam, &eaten); + if (textStore) { + textStore->OnEndHandlingKeyMessage(); + } + return SUCCEEDED(hr) && (eaten || !sKeystrokeMgr); + } + return false; +} + +// static +void +TSFTextStore::ProcessMessage(nsWindowBase* 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::GetInstance()->IsMSJapaneseIMEActive(); +} + +/******************************************************************/ +/* TSFTextStore::Composition */ +/******************************************************************/ + +void +TSFTextStore::Composition::Start(ITfCompositionView* aCompositionView, + LONG aCompositionStartOffset, + const nsAString& aCompositionString) +{ + mView = aCompositionView; + mString = aCompositionString; + mStart = aCompositionStartOffset; +} + +void +TSFTextStore::Composition::End() +{ + mView = nullptr; + mString.Truncate(); +} + +/****************************************************************************** + * TSFTextStore::Content + *****************************************************************************/ + +const nsDependentSubstring +TSFTextStore::Content::GetSelectedText() const +{ + MOZ_ASSERT(mInitialized); + 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 +{ + MOZ_ASSERT(mInitialized); + return nsDependentSubstring(mText, aStart, aLength); +} + +void +TSFTextStore::Content::ReplaceSelectedTextWith(const nsAString& aString) +{ + MOZ_ASSERT(mInitialized); + 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(mInitialized); + const nsDependentSubstring replacedString = + GetSubstring(static_cast<uint32_t>(aStart), + static_cast<uint32_t>(aLength)); + if (aReplaceString != replacedString) { + uint32_t firstDifferentOffset = mMinTextModifiedOffset; + if (mComposition.IsComposing()) { + // 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.mStart); + MOZ_ASSERT(aStart + aLength <= mComposition.EndOffset()); + mComposition.mString.Replace( + static_cast<uint32_t>(aStart - mComposition.mStart), + 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 + // mLastCompositionString. + if (mComposition.mString != mLastCompositionString) { + firstDifferentOffset = + mComposition.mStart + + FirstDifferentCharOffset(mComposition.mString, + mLastCompositionString); + // The previous change to the composition string is canceled. + if (mMinTextModifiedOffset >= + static_cast<uint32_t>(mComposition.mStart) && + mMinTextModifiedOffset < firstDifferentOffset) { + mMinTextModifiedOffset = firstDifferentOffset; + } + } else if (mMinTextModifiedOffset >= + static_cast<uint32_t>(mComposition.mStart) && + mMinTextModifiedOffset < + static_cast<uint32_t>(mComposition.EndOffset())) { + // The previous change to the composition string is canceled. + mMinTextModifiedOffset = firstDifferentOffset = + mComposition.EndOffset(); + } + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::Content::ReplaceTextWith(aStart=%d, " + "aLength=%d, aReplaceString=\"%s\"), mComposition={ mStart=%d, " + "mString=\"%s\" }, mLastCompositionString=\"%s\", " + "mMinTextModifiedOffset=%u, firstDifferentOffset=%u", + this, aStart, aLength, GetEscapedUTF8String(aReplaceString).get(), + mComposition.mStart, GetEscapedUTF8String(mComposition.mString).get(), + GetEscapedUTF8String(mLastCompositionString).get(), + mMinTextModifiedOffset, firstDifferentOffset)); + } else { + firstDifferentOffset = + static_cast<uint32_t>(aStart) + + FirstDifferentCharOffset(aReplaceString, replacedString); + } + mMinTextModifiedOffset = + std::min(mMinTextModifiedOffset, 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.CollapseAt( + static_cast<uint32_t>(aStart) + aReplaceString.Length()); +} + +void +TSFTextStore::Content::StartComposition(ITfCompositionView* aCompositionView, + const PendingAction& aCompStart, + bool aPreserveSelection) +{ + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(aCompositionView); + MOZ_ASSERT(!mComposition.mView); + MOZ_ASSERT(aCompStart.mType == PendingAction::COMPOSITION_START); + + mComposition.Start(aCompositionView, aCompStart.mSelectionStart, + GetSubstring(static_cast<uint32_t>(aCompStart.mSelectionStart), + static_cast<uint32_t>(aCompStart.mSelectionLength))); + 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. + mSelection.SetSelection(mComposition.mStart, mComposition.mString.Length(), + false, mSelection.GetWritingMode()); + } +} + +void +TSFTextStore::Content::RestoreCommittedComposition( + ITfCompositionView* aCompositionView, + const PendingAction& aPendingCompositionStart, + const PendingAction& aCanceledCompositionEnd) +{ + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(aCompositionView); + MOZ_ASSERT(!mComposition.mView); + MOZ_ASSERT(aPendingCompositionStart.mType == + PendingAction::COMPOSITION_START); + MOZ_ASSERT(aCanceledCompositionEnd.mType == + PendingAction::COMPOSITION_END); + MOZ_ASSERT(GetSubstring( + static_cast<uint32_t>(aPendingCompositionStart.mSelectionStart), + static_cast<uint32_t>(aCanceledCompositionEnd.mData.Length())) == + aCanceledCompositionEnd.mData); + + // Restore the committed string as composing string. + mComposition.Start(aCompositionView, + aPendingCompositionStart.mSelectionStart, + aCanceledCompositionEnd.mData); +} + +void +TSFTextStore::Content::EndComposition(const PendingAction& aCompEnd) +{ + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(mComposition.mView); + MOZ_ASSERT(aCompEnd.mType == PendingAction::COMPOSITION_END); + + mSelection.CollapseAt(mComposition.mStart + aCompEnd.mData.Length()); + mComposition.End(); +} + +/****************************************************************************** + * TSFTextStore::MouseTracker + *****************************************************************************/ + +TSFTextStore::MouseTracker::MouseTracker() + : mStart(-1) + , mLength(-1) + , mCookie(kInvalidCookie) +{ +} + +HRESULT +TSFTextStore::MouseTracker::Init(TSFTextStore* aTextStore) +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::Init(aTextStore=0x%p), " + "aTextStore->mMouseTrackers.Length()=%d", + this, aTextStore->mMouseTrackers.Length())); + + if (&aTextStore->mMouseTrackers.LastElement() != this) { + MOZ_LOG(sTextStoreLog, 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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::AdviseSink(aTextStore=0x%p, " + "aTextRange=0x%p, aMouseSink=0x%p), mCookie=%d, mSink=0x%p", + this, aTextStore, aTextRange, aMouseSink, mCookie, mSink.get())); + MOZ_ASSERT(mCookie != kInvalidCookie, "This hasn't been initalized?"); + + if (mSink) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to already being used", this)); + return E_FAIL; + } + + HRESULT hr = aTextRange->GetExtent(&mStart, &mLength); + if (FAILED(hr)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to failure of ITfRangeACP::GetExtent()", this)); + return hr; + } + + if (mStart < 0 || mLength <= 0) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to odd result of ITfRangeACP::GetExtent(), " + "mStart=%d, mLength=%d", this, mStart, mLength)); + return E_INVALIDARG; + } + + nsAutoString textContent; + if (NS_WARN_IF(!aTextStore->GetCurrentText(textContent))) { + MOZ_LOG(sTextStoreLog, 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>(mStart) || + textContent.Length() < static_cast<uint32_t>(mStart + mLength)) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to out of range, mStart=%d, mLength=%d, " + "textContent.Length()=%d", + this, mStart, mLength, textContent.Length())); + return E_INVALIDARG; + } + + mSink = aMouseSink; + + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink(), " + "succeeded, mStart=%d, mLength=%d, textContent.Length()=%d", + this, mStart, mLength, textContent.Length())); + return S_OK; +} + +void +TSFTextStore::MouseTracker::UnadviseSink() +{ + MOZ_LOG(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::UnadviseSink(), " + "mCookie=%d, mSink=0x%p, mStart=%d, mLength=%d", + this, mCookie, mSink.get(), mStart, mLength)); + mSink = nullptr; + mStart = mLength = -1; +} + +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(sTextStoreLog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::OnMouseEvent(aEdge=%d, " + "aQuadrant=%d, aButtonStatus=0x%08X), hr=0x%08X, eaten=%s", + this, aEdge, aQuadrant, aButtonStatus, hr, GetBoolName(!!eaten))); + + return SUCCEEDED(hr) && eaten; +} + +#ifdef DEBUG +// static +bool +TSFTextStore::CurrentKeyboardLayoutHasIME() +{ + if (!sInputProcessorProfiles) { + MOZ_LOG(sTextStoreLog, LogLevel::Error, + ("TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED due to " + "there is no input processor profiles instance")); + return false; + } + RefPtr<ITfInputProcessorProfileMgr> profileMgr; + HRESULT hr = + sInputProcessorProfiles->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(sTextStoreLog, 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(sTextStoreLog, LogLevel::Error, + (" TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED to retreive " + "active profile")); + return false; + } + return (profile.dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR); +} +#endif // #ifdef DEBUG + +} // name widget +} // name mozilla |