diff options
Diffstat (limited to 'widget/windows/TSFTextStore.h')
-rw-r--r-- | widget/windows/TSFTextStore.h | 1045 |
1 files changed, 1045 insertions, 0 deletions
diff --git a/widget/windows/TSFTextStore.h b/widget/windows/TSFTextStore.h new file mode 100644 index 000000000..9596510d5 --- /dev/null +++ b/widget/windows/TSFTextStore.h @@ -0,0 +1,1045 @@ +/* -*- 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/. */ + +#ifndef TSFTextStore_h_ +#define TSFTextStore_h_ + +#include "nsCOMPtr.h" +#include "nsIWidget.h" +#include "nsString.h" +#include "nsWindowBase.h" + +#include "WinUtils.h" +#include "WritingModes.h" + +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextRange.h" +#include "mozilla/WindowsVersion.h" + +#include <msctf.h> +#include <textstor.h> + +// GUID_PROP_INPUTSCOPE is declared in inputscope.h using INIT_GUID. +// With initguid.h, we get its instance instead of extern declaration. +#ifdef INPUTSCOPE_INIT_GUID +#include <initguid.h> +#endif +#ifdef TEXTATTRS_INIT_GUID +#include <tsattrs.h> +#endif +#include <inputscope.h> + +// TSF InputScope, for earlier SDK 8 +#define IS_SEARCH static_cast<InputScope>(50) + +struct ITfThreadMgr; +struct ITfDocumentMgr; +struct ITfDisplayAttributeMgr; +struct ITfCategoryMgr; +class nsWindow; + +namespace mozilla { +namespace widget { + +struct MSGResult; + +/* + * Text Services Framework text store + */ + +class TSFTextStore final : public ITextStoreACP + , public ITfContextOwnerCompositionSink + , public ITfMouseTrackerACP +{ +private: + typedef IMENotification::SelectionChangeDataBase SelectionChangeDataBase; + typedef IMENotification::SelectionChangeData SelectionChangeData; + typedef IMENotification::TextChangeDataBase TextChangeDataBase; + typedef IMENotification::TextChangeData TextChangeData; + +public: /*IUnknown*/ + STDMETHODIMP QueryInterface(REFIID, void**); + + NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(TSFTextStore) + +public: /*ITextStoreACP*/ + STDMETHODIMP AdviseSink(REFIID, IUnknown*, DWORD); + STDMETHODIMP UnadviseSink(IUnknown*); + STDMETHODIMP RequestLock(DWORD, HRESULT*); + STDMETHODIMP GetStatus(TS_STATUS*); + STDMETHODIMP QueryInsert(LONG, LONG, ULONG, LONG*, LONG*); + STDMETHODIMP GetSelection(ULONG, ULONG, TS_SELECTION_ACP*, ULONG*); + STDMETHODIMP SetSelection(ULONG, const TS_SELECTION_ACP*); + STDMETHODIMP GetText(LONG, LONG, WCHAR*, ULONG, ULONG*, TS_RUNINFO*, ULONG, + ULONG*, LONG*); + STDMETHODIMP SetText(DWORD, LONG, LONG, const WCHAR*, ULONG, TS_TEXTCHANGE*); + STDMETHODIMP GetFormattedText(LONG, LONG, IDataObject**); + STDMETHODIMP GetEmbedded(LONG, REFGUID, REFIID, IUnknown**); + STDMETHODIMP QueryInsertEmbedded(const GUID*, const FORMATETC*, BOOL*); + STDMETHODIMP InsertEmbedded(DWORD, LONG, LONG, IDataObject*, TS_TEXTCHANGE*); + STDMETHODIMP RequestSupportedAttrs(DWORD, ULONG, const TS_ATTRID*); + STDMETHODIMP RequestAttrsAtPosition(LONG, ULONG, const TS_ATTRID*, DWORD); + STDMETHODIMP RequestAttrsTransitioningAtPosition(LONG, ULONG, + const TS_ATTRID*, DWORD); + STDMETHODIMP FindNextAttrTransition(LONG, LONG, ULONG, const TS_ATTRID*, + DWORD, LONG*, BOOL*, LONG*); + STDMETHODIMP RetrieveRequestedAttrs(ULONG, TS_ATTRVAL*, ULONG*); + STDMETHODIMP GetEndACP(LONG*); + STDMETHODIMP GetActiveView(TsViewCookie*); + STDMETHODIMP GetACPFromPoint(TsViewCookie, const POINT*, DWORD, LONG*); + STDMETHODIMP GetTextExt(TsViewCookie, LONG, LONG, RECT*, BOOL*); + STDMETHODIMP GetScreenExt(TsViewCookie, RECT*); + STDMETHODIMP GetWnd(TsViewCookie, HWND*); + STDMETHODIMP InsertTextAtSelection(DWORD, const WCHAR*, ULONG, LONG*, LONG*, + TS_TEXTCHANGE*); + STDMETHODIMP InsertEmbeddedAtSelection(DWORD, IDataObject*, LONG*, LONG*, + TS_TEXTCHANGE*); + +public: /*ITfContextOwnerCompositionSink*/ + STDMETHODIMP OnStartComposition(ITfCompositionView*, BOOL*); + STDMETHODIMP OnUpdateComposition(ITfCompositionView*, ITfRange*); + STDMETHODIMP OnEndComposition(ITfCompositionView*); + +public: /*ITfMouseTrackerACP*/ + STDMETHODIMP AdviseMouseSink(ITfRangeACP*, ITfMouseSink*, DWORD*); + STDMETHODIMP UnadviseMouseSink(DWORD); + +public: + static void Initialize(void); + static void Terminate(void); + + static bool ProcessRawKeyMessage(const MSG& aMsg); + static void ProcessMessage(nsWindowBase* aWindow, UINT aMessage, + WPARAM& aWParam, LPARAM& aLParam, + MSGResult& aResult); + + static void SetIMEOpenState(bool); + static bool GetIMEOpenState(void); + + static void CommitComposition(bool aDiscard) + { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + textStore->CommitCompositionInternal(aDiscard); + } + + static void SetInputContext(nsWindowBase* aWidget, + const InputContext& aContext, + const InputContextAction& aAction); + + static nsresult OnFocusChange(bool aGotFocus, + nsWindowBase* aFocusedWidget, + const InputContext& aContext); + static nsresult OnTextChange(const IMENotification& aIMENotification) + { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + return textStore->OnTextChangeInternal(aIMENotification); + } + + static nsresult OnSelectionChange(const IMENotification& aIMENotification) + { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + return textStore->OnSelectionChangeInternal(aIMENotification); + } + + static nsresult OnLayoutChange() + { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + return textStore->OnLayoutChangeInternal(); + } + + static nsresult OnUpdateComposition() + { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + return textStore->OnUpdateCompositionInternal(); + } + + static nsresult OnMouseButtonEvent(const IMENotification& aIMENotification) + { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr<TSFTextStore> textStore(sEnabledTextStore); + return textStore->OnMouseButtonEventInternal(aIMENotification); + } + + static nsIMEUpdatePreference GetIMEUpdatePreference(); + + // Returns the address of the pointer so that the TSF automatic test can + // replace the system object with a custom implementation for testing. + // XXX TSF doesn't work now. Should we remove it? + static void* GetNativeData(uint32_t aDataType) + { + switch (aDataType) { + case NS_NATIVE_TSF_THREAD_MGR: + Initialize(); // Apply any previous changes + return static_cast<void*>(&sThreadMgr); + case NS_NATIVE_TSF_CATEGORY_MGR: + return static_cast<void*>(&sCategoryMgr); + case NS_NATIVE_TSF_DISPLAY_ATTR_MGR: + return static_cast<void*>(&sDisplayAttrMgr); + default: + return nullptr; + } + } + + static ITfMessagePump* GetMessagePump() + { + return sMessagePump; + } + + static void* GetThreadManager() + { + return static_cast<void*>(sThreadMgr); + } + + static bool ThinksHavingFocus() + { + return (sEnabledTextStore && sEnabledTextStore->mContext); + } + + static bool IsInTSFMode() + { + return sThreadMgr != nullptr; + } + + static bool IsComposing() + { + return (sEnabledTextStore && sEnabledTextStore->mComposition.IsComposing()); + } + + static bool IsComposingOn(nsWindowBase* aWidget) + { + return (IsComposing() && sEnabledTextStore->mWidget == aWidget); + } + + static nsWindowBase* GetEnabledWindowBase() + { + return sEnabledTextStore ? sEnabledTextStore->mWidget.get() : nullptr; + } + + /** + * Returns true if active keyboard layout is a legacy IMM-IME. + */ + static bool IsIMM_IMEActive(); + + /** + * Returns true if active TIP is MS-IME for Japanese. + */ + static bool IsMSJapaneseIMEActive(); + +#ifdef DEBUG + // Returns true when keyboard layout has IME (TIP). + static bool CurrentKeyboardLayoutHasIME(); +#endif // #ifdef DEBUG + +protected: + TSFTextStore(); + ~TSFTextStore(); + + static bool CreateAndSetFocus(nsWindowBase* aFocusedWidget, + const InputContext& aContext); + static void EnsureToDestroyAndReleaseEnabledTextStoreIf( + RefPtr<TSFTextStore>& aTextStore); + static void MarkContextAsKeyboardDisabled(ITfContext* aContext); + static void MarkContextAsEmpty(ITfContext* aContext); + + bool Init(nsWindowBase* aWidget, const InputContext& aContext); + void Destroy(); + void ReleaseTSFObjects(); + + bool IsReadLock(DWORD aLock) const + { + return (TS_LF_READ == (aLock & TS_LF_READ)); + } + bool IsReadWriteLock(DWORD aLock) const + { + return (TS_LF_READWRITE == (aLock & TS_LF_READWRITE)); + } + bool IsReadLocked() const { return IsReadLock(mLock); } + bool IsReadWriteLocked() const { return IsReadWriteLock(mLock); } + + // This is called immediately after a call of OnLockGranted() of mSink. + // Note that mLock isn't cleared yet when this is called. + void DidLockGranted(); + + bool GetScreenExtInternal(RECT& aScreenExt); + // If aDispatchCompositionChangeEvent is true, this method will dispatch + // compositionchange event if this is called during IME composing. + // aDispatchCompositionChangeEvent should be true only when this is called + // from SetSelection. Because otherwise, the compositionchange event should + // not be sent from here. + HRESULT SetSelectionInternal(const TS_SELECTION_ACP*, + bool aDispatchCompositionChangeEvent = false); + bool InsertTextAtSelectionInternal(const nsAString& aInsertStr, + TS_TEXTCHANGE* aTextChange); + void CommitCompositionInternal(bool); + HRESULT GetDisplayAttribute(ITfProperty* aProperty, + ITfRange* aRange, + TF_DISPLAYATTRIBUTE* aResult); + HRESULT RestartCompositionIfNecessary(ITfRange* pRangeNew = nullptr); + HRESULT RestartComposition(ITfCompositionView* aCompositionView, + ITfRange* aNewRange); + + // Following methods record composing action(s) to mPendingActions. + // They will be flushed FlushPendingActions(). + HRESULT RecordCompositionStartAction(ITfCompositionView* aCompositionView, + ITfRange* aRange, + bool aPreserveSelection); + HRESULT RecordCompositionStartAction(ITfCompositionView* aComposition, + LONG aStart, + LONG aLength, + bool aPreserveSelection); + HRESULT RecordCompositionUpdateAction(); + HRESULT RecordCompositionEndAction(); + + // DispatchEvent() dispatches the event and if it may not be handled + // synchronously, this makes the instance not notify TSF of pending + // notifications until next notification from content. + void DispatchEvent(WidgetGUIEvent& aEvent); + void OnLayoutInformationAvaliable(); + + // FlushPendingActions() performs pending actions recorded in mPendingActions + // and clear it. + void FlushPendingActions(); + // MaybeFlushPendingNotifications() performs pending notifications to TSF. + void MaybeFlushPendingNotifications(); + + nsresult OnTextChangeInternal(const IMENotification& aIMENotification); + nsresult OnSelectionChangeInternal(const IMENotification& aIMENotification); + nsresult OnMouseButtonEventInternal(const IMENotification& aIMENotification); + nsresult OnLayoutChangeInternal(); + nsresult OnUpdateCompositionInternal(); + + // mPendingSelectionChangeData stores selection change data until notifying + // TSF of selection change. If two or more selection changes occur, this + // stores the latest selection change data because only it is necessary. + SelectionChangeData mPendingSelectionChangeData; + + // mPendingTextChangeData stores one or more text change data until notifying + // TSF of text change. If two or more text changes occur, this merges + // every text change data. + TextChangeData mPendingTextChangeData; + + void NotifyTSFOfTextChange(); + void NotifyTSFOfSelectionChange(); + bool NotifyTSFOfLayoutChange(); + void NotifyTSFOfLayoutChangeAgain(); + + HRESULT HandleRequestAttrs(DWORD aFlags, + ULONG aFilterCount, + const TS_ATTRID* aFilterAttrs); + void SetInputScope(const nsString& aHTMLInputType, + const nsString& aHTMLInputInputmode); + + // Creates native caret over our caret. This method only works on desktop + // application. Otherwise, this does nothing. + void CreateNativeCaret(); + // Destroys native caret if there is. + void MaybeDestroyNativeCaret(); + + // Holds the pointer to our current win32 widget + RefPtr<nsWindowBase> mWidget; + // mDispatcher is a helper class to dispatch composition events. + RefPtr<TextEventDispatcher> mDispatcher; + // Document manager for the currently focused editor + RefPtr<ITfDocumentMgr> mDocumentMgr; + // Edit cookie associated with the current editing context + DWORD mEditCookie; + // Editing context at the bottom of mDocumentMgr's context stack + RefPtr<ITfContext> mContext; + // Currently installed notification sink + RefPtr<ITextStoreACPSink> mSink; + // TS_AS_* mask of what events to notify + DWORD mSinkMask; + // 0 if not locked, otherwise TS_LF_* indicating the current lock + DWORD mLock; + // 0 if no lock is queued, otherwise TS_LF_* indicating the queue lock + DWORD mLockQueued; + + uint32_t mHandlingKeyMessage; + void OnStartToHandleKeyMessage() { ++mHandlingKeyMessage; } + void OnEndHandlingKeyMessage() + { + MOZ_ASSERT(mHandlingKeyMessage); + if (--mHandlingKeyMessage) { + return; + } + // If TSFTextStore instance is destroyed during handling key message(s), + // release all TSF objects when all nested key messages have been handled. + if (mDestroyed) { + ReleaseTSFObjects(); + } + } + + class Composition final + { + public: + // nullptr if no composition is active, otherwise the current composition + RefPtr<ITfCompositionView> mView; + + // Current copy of the active composition string. Only mString is + // changed during a InsertTextAtSelection call if we have a composition. + // mString acts as a buffer until OnUpdateComposition is called + // and mString is flushed to editor through eCompositionChange. + // This way all changes are updated in batches to avoid + // inconsistencies/artifacts. + nsString mString; + + // The start of the current active composition, in ACP offsets + LONG mStart; + + bool IsComposing() const + { + return (mView != nullptr); + } + + LONG EndOffset() const + { + return mStart + static_cast<LONG>(mString.Length()); + } + + // Start() and End() updates the members for emulating the latest state. + // Unless flush the pending actions, this data never matches the actual + // content. + void Start(ITfCompositionView* aCompositionView, + LONG aCompositionStartOffset, + const nsAString& aCompositionString); + void End(); + }; + // While the document is locked, we cannot dispatch any events which cause + // DOM events since the DOM events' handlers may modify the locked document. + // However, even while the document is locked, TSF may queries us. + // For that, TSFTextStore modifies mComposition even while the document is + // locked. With mComposition, query methods can returns the text content + // information. + Composition mComposition; + + /** + * IsComposingInContent() returns true if there is a composition in the + * focused editor and it's caused by native IME (either TIP of TSF or IME of + * IMM). I.e., returns true between eCompositionStart and + * eCompositionCommit(AsIs). + */ + bool IsComposingInContent() const; + + class Selection + { + public: + Selection() : mDirty(true) {} + + bool IsDirty() const { return mDirty; }; + void MarkDirty() { mDirty = true; } + + TS_SELECTION_ACP& ACP() + { + MOZ_ASSERT(!mDirty); + return mACP; + } + + void SetSelection(const TS_SELECTION_ACP& aSelection) + { + mDirty = false; + mACP = aSelection; + // Selection end must be active in our editor. + if (mACP.style.ase != TS_AE_START) { + mACP.style.ase = TS_AE_END; + } + // We're not support interim char selection for now. + // XXX Probably, this is necessary for supporting South Asian languages. + mACP.style.fInterimChar = FALSE; + } + + bool SetSelection(uint32_t aStart, + uint32_t aLength, + bool aReversed, + WritingMode aWritingMode) + { + bool changed = mDirty || + mACP.acpStart != static_cast<LONG>(aStart) || + mACP.acpEnd != static_cast<LONG>(aStart + aLength); + + mDirty = false; + mACP.acpStart = static_cast<LONG>(aStart); + mACP.acpEnd = static_cast<LONG>(aStart + aLength); + mACP.style.ase = aReversed ? TS_AE_START : TS_AE_END; + mACP.style.fInterimChar = FALSE; + mWritingMode = aWritingMode; + + return changed; + } + + bool IsCollapsed() const + { + MOZ_ASSERT(!mDirty); + return (mACP.acpStart == mACP.acpEnd); + } + + void CollapseAt(uint32_t aOffset) + { + // XXX This does not update the selection's mWritingMode. + // If it is ever used to "collapse" to an entirely new location, + // we may need to fix that. + mDirty = false; + mACP.acpStart = mACP.acpEnd = static_cast<LONG>(aOffset); + mACP.style.ase = TS_AE_END; + mACP.style.fInterimChar = FALSE; + } + + LONG MinOffset() const + { + MOZ_ASSERT(!mDirty); + LONG min = std::min(mACP.acpStart, mACP.acpEnd); + MOZ_ASSERT(min >= 0); + return min; + } + + LONG MaxOffset() const + { + MOZ_ASSERT(!mDirty); + LONG max = std::max(mACP.acpStart, mACP.acpEnd); + MOZ_ASSERT(max >= 0); + return max; + } + + LONG StartOffset() const + { + MOZ_ASSERT(!mDirty); + MOZ_ASSERT(mACP.acpStart >= 0); + return mACP.acpStart; + } + + LONG EndOffset() const + { + MOZ_ASSERT(!mDirty); + MOZ_ASSERT(mACP.acpEnd >= 0); + return mACP.acpEnd; + } + + LONG Length() const + { + MOZ_ASSERT(!mDirty); + MOZ_ASSERT(mACP.acpEnd >= mACP.acpStart); + return std::abs(mACP.acpEnd - mACP.acpStart); + } + + bool IsReversed() const + { + MOZ_ASSERT(!mDirty); + return (mACP.style.ase == TS_AE_START); + } + + TsActiveSelEnd ActiveSelEnd() const + { + MOZ_ASSERT(!mDirty); + return mACP.style.ase; + } + + bool IsInterimChar() const + { + MOZ_ASSERT(!mDirty); + return (mACP.style.fInterimChar != FALSE); + } + + WritingMode GetWritingMode() const + { + MOZ_ASSERT(!mDirty); + return mWritingMode; + } + + bool EqualsExceptDirection(const TS_SELECTION_ACP& aACP) const + { + if (mACP.style.ase == aACP.style.ase) { + return mACP.acpStart == aACP.acpStart && + mACP.acpEnd == aACP.acpEnd; + } + return mACP.acpStart == aACP.acpEnd && + mACP.acpEnd == aACP.acpStart; + } + + bool EqualsExceptDirection( + const SelectionChangeDataBase& aChangedSelection) const + { + MOZ_ASSERT(!mDirty); + MOZ_ASSERT(aChangedSelection.IsValid()); + return aChangedSelection.Length() == static_cast<uint32_t>(Length()) && + aChangedSelection.mOffset == static_cast<uint32_t>(StartOffset()); + } + + private: + TS_SELECTION_ACP mACP; + WritingMode mWritingMode; + bool mDirty; + }; + // Don't access mSelection directly except at calling MarkDirty(). + // Use SelectionForTSFRef() instead. This is modified immediately when + // TSF requests to set selection and not updated by selection change in + // content until mContentForTSF is cleared. + Selection mSelectionForTSF; + + /** + * Get the selection expected by TSF. If mSelectionForTSF is already valid, + * this just return the reference to it. Otherwise, this initializes it + * with eQuerySelectedText. Please check if the result is valid before + * actually using it. + * Note that this is also called by ContentForTSFRef(). + */ + Selection& SelectionForTSFRef(); + + class MOZ_STACK_CLASS AutoSetTemporarySelection final + { + public: + AutoSetTemporarySelection(Selection& aSelection) + : mSelection(aSelection) + { + mDirty = mSelection.IsDirty(); + if (mDirty) { + mSelection.CollapseAt(0); + } + } + + ~AutoSetTemporarySelection() + { + if (mDirty) { + mSelection.MarkDirty(); + } + } + + private: + Selection& mSelection; + bool mDirty; + }; + + struct PendingAction final + { + enum ActionType : uint8_t + { + COMPOSITION_START, + COMPOSITION_UPDATE, + COMPOSITION_END, + SET_SELECTION + }; + ActionType mType; + // For compositionstart and selectionset + LONG mSelectionStart; + LONG mSelectionLength; + // For compositionstart, compositionupdate and compositionend + nsString mData; + // For compositionupdate + RefPtr<TextRangeArray> mRanges; + // For selectionset + bool mSelectionReversed; + // For compositionupdate + bool mIncomplete; + // For compositionstart + bool mAdjustSelection; + }; + // Items of mPendingActions are appended when TSF tells us to need to dispatch + // DOM composition events. However, we cannot dispatch while the document is + // locked because it can cause modifying the locked document. So, the pending + // actions should be performed when document lock is unlocked. + nsTArray<PendingAction> mPendingActions; + + PendingAction* LastOrNewPendingCompositionUpdate() + { + if (!mPendingActions.IsEmpty()) { + PendingAction& lastAction = mPendingActions.LastElement(); + if (lastAction.mType == PendingAction::COMPOSITION_UPDATE) { + return &lastAction; + } + } + PendingAction* newAction = mPendingActions.AppendElement(); + newAction->mType = PendingAction::COMPOSITION_UPDATE; + newAction->mRanges = new TextRangeArray(); + newAction->mIncomplete = true; + return newAction; + } + + /** + * WasTextInsertedWithoutCompositionAt() checks if text was inserted without + * composition immediately before (e.g., see InsertTextAtSelectionInternal()). + * + * @param aStart The inserted offset you expected. + * @param aLength The inserted text length you expected. + * @return true if the last pending actions are + * COMPOSITION_START and COMPOSITION_END and + * aStart and aLength match their information. + */ + bool WasTextInsertedWithoutCompositionAt(LONG aStart, LONG aLength) const + { + if (mPendingActions.Length() < 2) { + return false; + } + const PendingAction& pendingLastAction = mPendingActions.LastElement(); + if (pendingLastAction.mType != PendingAction::COMPOSITION_END || + pendingLastAction.mData.Length() != aLength) { + return false; + } + const PendingAction& pendingPreLastAction = + mPendingActions[mPendingActions.Length() - 2]; + return pendingPreLastAction.mType == PendingAction::COMPOSITION_START && + pendingPreLastAction.mSelectionStart == aStart; + } + + bool IsPendingCompositionUpdateIncomplete() const + { + if (mPendingActions.IsEmpty()) { + return false; + } + const PendingAction& lastAction = mPendingActions.LastElement(); + return lastAction.mType == PendingAction::COMPOSITION_UPDATE && + lastAction.mIncomplete; + } + + void CompleteLastActionIfStillIncomplete() + { + if (!IsPendingCompositionUpdateIncomplete()) { + return; + } + RecordCompositionUpdateAction(); + } + + // When On*Composition() is called without document lock, we need to flush + // the recorded actions at quitting the method. + // AutoPendingActionAndContentFlusher class is usedful for it. + class MOZ_STACK_CLASS AutoPendingActionAndContentFlusher final + { + public: + AutoPendingActionAndContentFlusher(TSFTextStore* aTextStore) + : mTextStore(aTextStore) + { + MOZ_ASSERT(!mTextStore->mIsRecordingActionsWithoutLock); + if (!mTextStore->IsReadWriteLocked()) { + mTextStore->mIsRecordingActionsWithoutLock = true; + } + } + + ~AutoPendingActionAndContentFlusher() + { + if (!mTextStore->mIsRecordingActionsWithoutLock) { + return; + } + mTextStore->FlushPendingActions(); + mTextStore->mIsRecordingActionsWithoutLock = false; + } + + private: + AutoPendingActionAndContentFlusher() {} + + RefPtr<TSFTextStore> mTextStore; + }; + + class Content final + { + public: + Content(TSFTextStore::Composition& aComposition, + TSFTextStore::Selection& aSelection) : + mComposition(aComposition), mSelection(aSelection) + { + Clear(); + } + + void Clear() + { + mText.Truncate(); + mLastCompositionString.Truncate(); + mInitialized = false; + } + + bool IsInitialized() const { return mInitialized; } + + void Init(const nsAString& aText) + { + mText = aText; + if (mComposition.IsComposing()) { + mLastCompositionString = mComposition.mString; + } else { + mLastCompositionString.Truncate(); + } + mMinTextModifiedOffset = NOT_MODIFIED; + mInitialized = true; + } + + void OnLayoutChanged() + { + mMinTextModifiedOffset = NOT_MODIFIED; + } + + const nsDependentSubstring GetSelectedText() const; + const nsDependentSubstring GetSubstring(uint32_t aStart, + uint32_t aLength) const; + void ReplaceSelectedTextWith(const nsAString& aString); + void ReplaceTextWith(LONG aStart, LONG aLength, const nsAString& aString); + + void StartComposition(ITfCompositionView* aCompositionView, + const PendingAction& aCompStart, + bool aPreserveSelection); + /** + * RestoreCommittedComposition() restores the committed string as + * composing string. If InsertTextAtSelection() or something is called + * before a call of OnStartComposition(), there is a pending + * compositionstart and a pending compositionend. In this case, we + * need to cancel the pending compositionend and continue the composition. + * + * @param aCompositionView The composition view. + * @param aPendingCompositionStart The pending compositionstart which + * started the committed composition. + * @param aCanceledCompositionEnd The pending compositionend which is + * canceled for restarting the composition. + */ + void RestoreCommittedComposition( + ITfCompositionView* aCompositionView, + const PendingAction& aPendingCompositionStart, + const PendingAction& aCanceledCompositionEnd); + void EndComposition(const PendingAction& aCompEnd); + + const nsString& Text() const + { + MOZ_ASSERT(mInitialized); + return mText; + } + const nsString& LastCompositionString() const + { + MOZ_ASSERT(mInitialized); + return mLastCompositionString; + } + uint32_t MinTextModifiedOffset() const + { + MOZ_ASSERT(mInitialized); + return mMinTextModifiedOffset; + } + + // Returns true if layout of the character at the aOffset has not been + // calculated. + bool IsLayoutChangedAt(uint32_t aOffset) const + { + return IsLayoutChanged() && (mMinTextModifiedOffset <= aOffset); + } + // Returns true if layout of the content has been changed, i.e., the new + // layout has not been calculated. + bool IsLayoutChanged() const + { + return mInitialized && (mMinTextModifiedOffset != NOT_MODIFIED); + } + // Returns minimum offset of modified text range. + uint32_t MinOffsetOfLayoutChanged() const + { + return mInitialized ? mMinTextModifiedOffset : NOT_MODIFIED; + } + + TSFTextStore::Composition& Composition() { return mComposition; } + TSFTextStore::Selection& Selection() { return mSelection; } + + private: + nsString mText; + // mLastCompositionString stores the composition string when the document + // is locked. This is necessary to compute mMinTextModifiedOffset. + nsString mLastCompositionString; + TSFTextStore::Composition& mComposition; + TSFTextStore::Selection& mSelection; + + // The minimum offset of modified part of the text. + enum : uint32_t + { + NOT_MODIFIED = UINT32_MAX + }; + uint32_t mMinTextModifiedOffset; + + bool mInitialized; + }; + // mContentForTSF is cache of content. The information is expected by TSF + // and TIP. Therefore, this is useful for answering the query from TSF or + // TIP. + // This is initialized by ContentForTSFRef() automatically (therefore, don't + // access this member directly except at calling Clear(), IsInitialized(), + // IsLayoutChangeAfter() or IsLayoutChanged()). + // This is cleared when: + // - When there is no composition, the document is unlocked. + // - When there is a composition, all dispatched events are handled by + // the focused editor which may be in a remote process. + // So, if two compositions are created very quickly, this cache may not be + // cleared between eCompositionCommit(AsIs) and eCompositionStart. + Content mContentForTSF; + + Content& ContentForTSFRef(); + + // CanAccessActualContentDirectly() returns true when TSF/TIP can access + // actual content directly. In other words, mContentForTSF and/or + // mSelectionForTSF doesn't cache content or they matches with actual + // contents due to no pending text/selection change notifications. + bool CanAccessActualContentDirectly() const; + + // While mContentForTSF is valid, this returns the text stored by it. + // Otherwise, return the current text content retrieved by eQueryTextContent. + bool GetCurrentText(nsAString& aTextContent); + + class MouseTracker final + { + public: + static const DWORD kInvalidCookie = static_cast<DWORD>(-1); + + MouseTracker(); + + HRESULT Init(TSFTextStore* aTextStore); + HRESULT AdviseSink(TSFTextStore* aTextStore, + ITfRangeACP* aTextRange, ITfMouseSink* aMouseSink); + void UnadviseSink(); + + bool IsUsing() const { return mSink != nullptr; } + bool InRange(uint32_t aOffset) const + { + if (NS_WARN_IF(mStart < 0) || + NS_WARN_IF(mLength <= 0)) { + return false; + } + return aOffset >= static_cast<uint32_t>(mStart) && + aOffset < static_cast<uint32_t>(mStart + mLength); + } + DWORD Cookie() const { return mCookie; } + bool OnMouseButtonEvent(ULONG aEdge, ULONG aQuadrant, DWORD aButtonStatus); + LONG RangeStart() const { return mStart; } + + private: + RefPtr<ITfMouseSink> mSink; + LONG mStart; + LONG mLength; + DWORD mCookie; + }; + // mMouseTrackers is an array to store each information of installed + // ITfMouseSink instance. + nsTArray<MouseTracker> mMouseTrackers; + + // The input scopes for this context, defaults to IS_DEFAULT. + nsTArray<InputScope> mInputScopes; + + // Support retrieving attributes. + // TODO: We should support RightToLeft, perhaps. + enum + { + // Used for result of GetRequestedAttrIndex() + eNotSupported = -1, + + // Supported attributes + eInputScope = 0, + eTextVerticalWriting, + eTextOrientation, + + // Count of the supported attributes + NUM_OF_SUPPORTED_ATTRS + }; + bool mRequestedAttrs[NUM_OF_SUPPORTED_ATTRS]; + + int32_t GetRequestedAttrIndex(const TS_ATTRID& aAttrID); + TS_ATTRID GetAttrID(int32_t aIndex); + + bool mRequestedAttrValues; + + // If edit actions are being recorded without document lock, this is true. + // Otherwise, false. + bool mIsRecordingActionsWithoutLock; + // If GetTextExt() or GetACPFromPoint() is called and the layout hasn't been + // calculated yet, these methods return TS_E_NOLAYOUT. At that time, + // mHasReturnedNoLayoutError is set to true. + bool mHasReturnedNoLayoutError; + // Before calling ITextStoreACPSink::OnLayoutChange() and + // ITfContextOwnerServices::OnLayoutChange(), mWaitingQueryLayout is set to + // true. This is set to false when GetTextExt() or GetACPFromPoint() is + // called. + bool mWaitingQueryLayout; + // During the documet is locked, we shouldn't destroy the instance. + // If this is true, the instance will be destroyed after unlocked. + bool mPendingDestroy; + // If this is false, MaybeFlushPendingNotifications() will clear the + // mContentForTSF. + bool mDeferClearingContentForTSF; + // While there is native caret, this is true. Otherwise, false. + bool mNativeCaretIsCreated; + // While the instance is dispatching events, the event may not be handled + // synchronously in e10s mode. So, in such case, in strictly speaking, + // we shouldn't query layout information. However, TS_E_NOLAYOUT bugs of + // ITextStoreAPC::GetTextExt() blocks us to behave ideally. + // For preventing it to be called, we should put off notifying TSF of + // anything until layout information becomes available. + bool mDeferNotifyingTSF; + // While the document is locked, committing composition always fails since + // TSF needs another document lock for modifying the composition, selection + // and etc. So, committing composition should be performed after the + // document is unlocked. + bool mDeferCommittingComposition; + bool mDeferCancellingComposition; + // Immediately after a call of Destroy(), mDestroyed becomes true. If this + // is true, the instance shouldn't grant any requests from the TIP anymore. + bool mDestroyed; + // While the instance is being destroyed, this is set to true for avoiding + // recursive Destroy() calls. + bool mBeingDestroyed; + + + // TSF thread manager object for the current application + static StaticRefPtr<ITfThreadMgr> sThreadMgr; + // sMessagePump is QI'ed from sThreadMgr + static StaticRefPtr<ITfMessagePump> sMessagePump; + // sKeystrokeMgr is QI'ed from sThreadMgr + static StaticRefPtr<ITfKeystrokeMgr> sKeystrokeMgr; + // TSF display attribute manager + static StaticRefPtr<ITfDisplayAttributeMgr> sDisplayAttrMgr; + // TSF category manager + static StaticRefPtr<ITfCategoryMgr> sCategoryMgr; + + // Current text store which is managing a keyboard enabled editor (i.e., + // editable editor). Currently only ONE TSFTextStore instance is ever used, + // although Create is called when an editor is focused and Destroy called + // when the focused editor is blurred. + static StaticRefPtr<TSFTextStore> sEnabledTextStore; + + // For IME (keyboard) disabled state: + static StaticRefPtr<ITfDocumentMgr> sDisabledDocumentMgr; + static StaticRefPtr<ITfContext> sDisabledContext; + + static StaticRefPtr<ITfInputProcessorProfiles> sInputProcessorProfiles; + + // TSF client ID for the current application + static DWORD sClientId; + + // Enables/Disables hack for specific TIP. + static bool sCreateNativeCaretForLegacyATOK; + static bool sDoNotReturnNoLayoutErrorToATOKOfCompositionString; + static bool sDoNotReturnNoLayoutErrorToMSSimplifiedTIP; + static bool sDoNotReturnNoLayoutErrorToMSTraditionalTIP; + static bool sDoNotReturnNoLayoutErrorToFreeChangJie; + static bool sDoNotReturnNoLayoutErrorToEasyChangjei; + static bool sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar; + static bool sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret; + static bool sHackQueryInsertForMSSimplifiedTIP; + static bool sHackQueryInsertForMSTraditionalTIP; +}; + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef TSFTextStore_h_ |